Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-9.4.x-1036-SchedulerThreads

This commit is contained in:
Greg Wilkins 2019-09-17 12:36:23 +10:00
commit bcf6b4c581
42 changed files with 2829 additions and 512 deletions

View File

@ -299,6 +299,11 @@
<artifactId>jetty-security</artifactId>
<version>9.4.21-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-openid</artifactId>
<version>9.4.21-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>

View File

@ -711,6 +711,11 @@
<artifactId>jetty-alpn-openjdk8-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-openid</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-conscrypt-server</artifactId>

View File

@ -89,7 +89,7 @@ public class ServletPathSpec extends PathSpec
{
this.group = PathSpecGroup.EXACT;
this.prefix = servletPathSpec;
if (servletPathSpec.endsWith("*") )
if (servletPathSpec.endsWith("*"))
{
LOG.warn("Suspicious URL pattern: '{}'; see sections 12.1 and 12.2 of the Servlet specification",
servletPathSpec);

View File

@ -461,6 +461,8 @@ public class HpackContext
if (value != null && value.length() > 0)
{
int huffmanLen = Huffman.octetsNeeded(value);
if (huffmanLen < 0)
throw new IllegalStateException("bad value");
int lenLen = NBitInteger.octectsNeeded(7, huffmanLen);
_huffmanValue = new byte[1 + lenLen + huffmanLen];
ByteBuffer buffer = ByteBuffer.wrap(_huffmanValue);

View File

@ -177,7 +177,7 @@ public class HpackDecoder
else
name = toASCIIString(buffer, length);
check:
for (int i = name.length(); i-- > 0;)
for (int i = name.length(); i-- > 0; )
{
char c = name.charAt(i);
if (c > 0xff)

View File

@ -19,9 +19,10 @@
package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
@ -33,24 +34,22 @@ import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
import org.eclipse.jetty.http2.hpack.HpackContext.StaticEntry;
import org.eclipse.jetty.util.ArrayTrie;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.Trie;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class HpackEncoder
{
public static final Logger LOG = Log.getLogger(HpackEncoder.class);
private static final HttpField[] __status = new HttpField[599];
static final EnumSet<HttpHeader> __DO_NOT_HUFFMAN =
private static final Logger LOG = Log.getLogger(HpackEncoder.class);
private static final HttpField[] STATUSES = new HttpField[599];
static final EnumSet<HttpHeader> DO_NOT_HUFFMAN =
EnumSet.of(
HttpHeader.AUTHORIZATION,
HttpHeader.CONTENT_MD5,
HttpHeader.PROXY_AUTHENTICATE,
HttpHeader.PROXY_AUTHORIZATION);
static final EnumSet<HttpHeader> __DO_NOT_INDEX =
static final EnumSet<HttpHeader> DO_NOT_INDEX =
EnumSet.of(
// HttpHeader.C_PATH, // TODO more data needed
// HttpHeader.DATE, // TODO more data needed
@ -70,23 +69,21 @@ public class HpackEncoder
HttpHeader.LAST_MODIFIED,
HttpHeader.SET_COOKIE,
HttpHeader.SET_COOKIE2);
static final EnumSet<HttpHeader> __NEVER_INDEX =
static final EnumSet<HttpHeader> NEVER_INDEX =
EnumSet.of(
HttpHeader.AUTHORIZATION,
HttpHeader.SET_COOKIE,
HttpHeader.SET_COOKIE2);
private static final PreEncodedHttpField CONNECTION_TE = new PreEncodedHttpField(HttpHeader.CONNECTION, "te");
private static final EnumSet<HttpHeader> IGNORED_HEADERS = EnumSet.of(HttpHeader.CONNECTION, HttpHeader.KEEP_ALIVE,
HttpHeader.PROXY_CONNECTION, HttpHeader.TRANSFER_ENCODING, HttpHeader.UPGRADE);
private static final PreEncodedHttpField TE_TRAILERS = new PreEncodedHttpField(HttpHeader.TE, "trailers");
private static final Trie<Boolean> specialHopHeaders = new ArrayTrie<>(6);
static
{
for (HttpStatus.Code code : HttpStatus.Code.values())
{
__status[code.getCode()] = new PreEncodedHttpField(HttpHeader.C_STATUS, Integer.toString(code.getCode()));
STATUSES[code.getCode()] = new PreEncodedHttpField(HttpHeader.C_STATUS, Integer.toString(code.getCode()));
}
specialHopHeaders.put("close", true);
specialHopHeaders.put("te", true);
}
private final HpackContext _context;
@ -174,33 +171,37 @@ public class HpackEncoder
{
MetaData.Response response = (MetaData.Response)metadata;
int code = response.getStatus();
HttpField status = code < __status.length ? __status[code] : null;
HttpField status = code < STATUSES.length ? STATUSES[code] : null;
if (status == null)
status = new HttpField.IntValueHttpField(HttpHeader.C_STATUS, code);
encode(buffer, status);
}
// Add all non-connection fields.
// Remove fields as specified in RFC 7540, 8.1.2.2.
HttpFields fields = metadata.getFields();
if (fields != null)
{
Set<String> hopHeaders = fields.getCSV(HttpHeader.CONNECTION, false).stream()
.filter(v -> specialHopHeaders.get(v) == Boolean.TRUE)
.map(StringUtil::asciiToLowerCase)
.collect(Collectors.toSet());
// For example: Connection: Close, TE, Upgrade, Custom.
Set<String> hopHeaders = null;
for (String value : fields.getCSV(HttpHeader.CONNECTION, false))
{
if (hopHeaders == null)
hopHeaders = new HashSet<>();
hopHeaders.add(StringUtil.asciiToLowerCase(value));
}
for (HttpField field : fields)
{
if (field.getHeader() == HttpHeader.CONNECTION)
HttpHeader header = field.getHeader();
if (header != null && IGNORED_HEADERS.contains(header))
continue;
if (!hopHeaders.isEmpty() && hopHeaders.contains(StringUtil.asciiToLowerCase(field.getName())))
continue;
if (field.getHeader() == HttpHeader.TE)
if (header == HttpHeader.TE)
{
if (!field.contains("trailers"))
continue;
encode(buffer, CONNECTION_TE);
encode(buffer, TE_TRAILERS);
if (field.contains("trailers"))
encode(buffer, TE_TRAILERS);
continue;
}
if (hopHeaders != null && hopHeaders.contains(StringUtil.asciiToLowerCase(field.getName())))
continue;
encode(buffer, field);
}
}
@ -319,12 +320,12 @@ public class HpackEncoder
if (_debug)
encoding = indexed ? "PreEncodedIdx" : "PreEncoded";
}
else if (__DO_NOT_INDEX.contains(header))
else if (DO_NOT_INDEX.contains(header))
{
// Non indexed field
indexed = false;
boolean neverIndex = __NEVER_INDEX.contains(header);
boolean huffman = !__DO_NOT_HUFFMAN.contains(header);
boolean neverIndex = NEVER_INDEX.contains(header);
boolean huffman = !DO_NOT_HUFFMAN.contains(header);
encodeName(buffer, neverIndex ? (byte)0x10 : (byte)0x00, 4, header.asString(), name);
encodeValue(buffer, huffman, field.getValue());
@ -347,7 +348,7 @@ public class HpackEncoder
{
// indexed
indexed = true;
boolean huffman = !__DO_NOT_HUFFMAN.contains(header);
boolean huffman = !DO_NOT_HUFFMAN.contains(header);
encodeName(buffer, (byte)0x40, 6, header.asString(), name);
encodeValue(buffer, huffman, field.getValue());
if (_debug)
@ -392,19 +393,38 @@ public class HpackEncoder
{
// huffman literal value
buffer.put((byte)0x80);
NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(value));
Huffman.encode(buffer, value);
int needed = Huffman.octetsNeeded(value);
if (needed >= 0)
{
NBitInteger.encode(buffer, 7, needed);
Huffman.encode(buffer, value);
}
else
{
// Not iso_8859_1
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(bytes));
Huffman.encode(buffer, bytes);
}
}
else
{
// add literal assuming iso_8859_1
buffer.put((byte)0x00);
buffer.put((byte)0x00).mark();
NBitInteger.encode(buffer, 7, value.length());
for (int i = 0; i < value.length(); i++)
{
char c = value.charAt(i);
if (c < ' ' || c > 127)
throw new IllegalArgumentException();
{
// Not iso_8859_1, so re-encode as UTF-8
buffer.reset();
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NBitInteger.encode(buffer, 7, bytes.length);
buffer.put(bytes, 0, bytes.length);
return;
}
buffer.put((byte)c);
}
}

View File

@ -46,7 +46,7 @@ public class HpackFieldPreEncoder implements HttpFieldPreEncoder
@Override
public byte[] getEncodedField(HttpHeader header, String name, String value)
{
boolean notIndexed = HpackEncoder.__DO_NOT_INDEX.contains(header);
boolean notIndexed = HpackEncoder.DO_NOT_INDEX.contains(header);
ByteBuffer buffer = BufferUtil.allocate(name.length() + value.length() + 10);
BufferUtil.clearToFill(buffer);
@ -56,8 +56,8 @@ public class HpackFieldPreEncoder implements HttpFieldPreEncoder
if (notIndexed)
{
// Non indexed field
boolean neverIndex = HpackEncoder.__NEVER_INDEX.contains(header);
huffman = !HpackEncoder.__DO_NOT_HUFFMAN.contains(header);
boolean neverIndex = HpackEncoder.NEVER_INDEX.contains(header);
huffman = !HpackEncoder.DO_NOT_HUFFMAN.contains(header);
buffer.put(neverIndex ? (byte)0x10 : (byte)0x00);
bits = 4;
}
@ -72,7 +72,7 @@ public class HpackFieldPreEncoder implements HttpFieldPreEncoder
{
// indexed
buffer.put((byte)0x40);
huffman = !HpackEncoder.__DO_NOT_HUFFMAN.contains(header);
huffman = !HpackEncoder.DO_NOT_HUFFMAN.contains(header);
bits = 6;
}

View File

@ -20,6 +20,8 @@ package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer;
import org.eclipse.jetty.util.Utf8StringBuilder;
public class Huffman
{
@ -358,7 +360,7 @@ public class Huffman
public static String decode(ByteBuffer buffer, int length) throws HpackException.CompressionException
{
StringBuilder out = new StringBuilder(length * 2);
Utf8StringBuilder utf8 = new Utf8StringBuilder(length * 2);
int node = 0;
int current = 0;
int bits = 0;
@ -378,7 +380,7 @@ public class Huffman
throw new HpackException.CompressionException("EOS in content");
// terminal node
out.append(rowsym[node]);
utf8.append((byte)(0xFF & rowsym[node]));
bits -= rowbits[node];
node = 0;
}
@ -411,7 +413,7 @@ public class Huffman
break;
}
out.append(rowsym[node]);
utf8.append((byte)(0xFF & rowsym[node]));
bits -= rowbits[node];
node = 0;
}
@ -419,7 +421,7 @@ public class Huffman
if (node != 0)
throw new HpackException.CompressionException("Bad termination");
return out.toString();
return utf8.toString();
}
public static int octetsNeeded(String s)
@ -427,11 +429,21 @@ public class Huffman
return octetsNeeded(CODES, s);
}
public static int octetsNeeded(byte[] b)
{
return octetsNeeded(CODES, b);
}
public static void encode(ByteBuffer buffer, String s)
{
encode(CODES, buffer, s);
}
public static void encode(ByteBuffer buffer, byte[] b)
{
encode(CODES, buffer, b);
}
public static int octetsNeededLC(String s)
{
return octetsNeeded(LCCODES, s);
@ -450,13 +462,30 @@ public class Huffman
{
char c = s.charAt(i);
if (c >= 128 || c < ' ')
throw new IllegalArgumentException();
return -1;
needed += table[c][1];
}
return (needed + 7) / 8;
}
private static int octetsNeeded(final int[][] table, byte[] b)
{
int needed = 0;
int len = b.length;
for (int i = 0; i < len; i++)
{
int c = 0xFF & b[i];
needed += table[c][1];
}
return (needed + 7) / 8;
}
/**
* @param table The table to encode by
* @param buffer The buffer to encode to
* @param s The string to encode
*/
private static void encode(final int[][] table, ByteBuffer buffer, String s)
{
long current = 0;
@ -488,4 +517,35 @@ public class Huffman
buffer.put((byte)(current));
}
}
private static void encode(final int[][] table, ByteBuffer buffer, byte[] b)
{
long current = 0;
int n = 0;
int len = b.length;
for (int i = 0; i < len; i++)
{
int c = 0xFF & b[i];
int code = table[c][0];
int bits = table[c][1];
current <<= bits;
current |= code;
n += bits;
while (n >= 8)
{
n -= 8;
buffer.put((byte)(current >> n));
}
}
if (n > 0)
{
current <<= (8 - n);
current |= (0xFF >>> n);
buffer.put((byte)(current));
}
}
}

View File

@ -67,7 +67,7 @@ public class HpackTest
BufferUtil.flipToFlush(buffer, 0);
Response decoded0 = (Response)decoder.decode(buffer);
original0.getFields().put(new HttpField(HttpHeader.CONTENT_ENCODING, ""));
assertMetadataSame(original0, decoded0);
assertMetaDataResponseSame(original0, decoded0);
// Same again?
BufferUtil.clearToFill(buffer);
@ -75,7 +75,7 @@ public class HpackTest
BufferUtil.flipToFlush(buffer, 0);
Response decoded0b = (Response)decoder.decode(buffer);
assertMetadataSame(original0, decoded0b);
assertMetaDataResponseSame(original0, decoded0b);
HttpFields fields1 = new HttpFields();
fields1.add(HttpHeader.CONTENT_TYPE, "text/plain");
@ -93,7 +93,7 @@ public class HpackTest
BufferUtil.flipToFlush(buffer, 0);
Response decoded1 = (Response)decoder.decode(buffer);
assertMetadataSame(original1, decoded1);
assertMetaDataResponseSame(original1, decoded1);
assertEquals("custom-key", decoded1.getFields().getField("Custom-Key").getName());
}
@ -106,19 +106,19 @@ public class HpackTest
HttpFields fields0 = new HttpFields();
fields0.add("1234567890", "1234567890123456789012345678901234567890");
fields0.add("Cookie", "abcdeffhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR");
fields0.add("Cookie", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR");
MetaData original0 = new MetaData(HttpVersion.HTTP_2, fields0);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original0);
BufferUtil.flipToFlush(buffer, 0);
MetaData decoded0 = (MetaData)decoder.decode(buffer);
MetaData decoded0 = decoder.decode(buffer);
assertMetadataSame(original0, decoded0);
assertMetaDataSame(original0, decoded0);
HttpFields fields1 = new HttpFields();
fields1.add("1234567890", "1234567890123456789012345678901234567890");
fields1.add("Cookie", "abcdeffhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR");
fields1.add("Cookie", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR");
fields1.add("x", "y");
MetaData original1 = new MetaData(HttpVersion.HTTP_2, fields1);
@ -136,6 +136,26 @@ public class HpackTest
}
}
@Test
public void encodeDecodeNonAscii() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096, 8192);
ByteBuffer buffer = BufferUtil.allocate(16 * 1024);
HttpFields fields0 = new HttpFields();
fields0.add("Cookie", "[\uD842\uDF9F]");
fields0.add("custom-key", "[\uD842\uDF9F]");
Response original0 = new MetaData.Response(HttpVersion.HTTP_2, 200, fields0);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original0);
BufferUtil.flipToFlush(buffer, 0);
Response decoded0 = (Response)decoder.decode(buffer);
assertMetaDataSame(original0, decoded0);
}
@Test
public void evictReferencedFieldTest() throws Exception
{
@ -143,57 +163,111 @@ public class HpackTest
HpackDecoder decoder = new HpackDecoder(200, 1024);
ByteBuffer buffer = BufferUtil.allocateDirect(16 * 1024);
String longEnoughToBeEvicted = "012345678901234567890123456789012345678901234567890";
HttpFields fields0 = new HttpFields();
fields0.add("123456789012345678901234567890123456788901234567890", "value");
fields0.add("foo", "abcdeffhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR");
fields0.add(longEnoughToBeEvicted, "value");
fields0.add("foo", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
MetaData original0 = new MetaData(HttpVersion.HTTP_2, fields0);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original0);
BufferUtil.flipToFlush(buffer, 0);
MetaData decoded0 = (MetaData)decoder.decode(buffer);
MetaData decoded0 = decoder.decode(buffer);
assertEquals(2, encoder.getHpackContext().size());
assertEquals(2, decoder.getHpackContext().size());
assertEquals("123456789012345678901234567890123456788901234567890", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 1).getHttpField().getName());
assertEquals("foo", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 0).getHttpField().getName());
assertEquals(longEnoughToBeEvicted, encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 1).getHttpField().getName());
assertEquals("foo", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length).getHttpField().getName());
assertMetadataSame(original0, decoded0);
assertMetaDataSame(original0, decoded0);
HttpFields fields1 = new HttpFields();
fields1.add("123456789012345678901234567890123456788901234567890", "other_value");
fields1.add(longEnoughToBeEvicted, "other_value");
fields1.add("x", "y");
MetaData original1 = new MetaData(HttpVersion.HTTP_2, fields1);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original1);
BufferUtil.flipToFlush(buffer, 0);
MetaData decoded1 = (MetaData)decoder.decode(buffer);
assertMetadataSame(original1, decoded1);
MetaData decoded1 = decoder.decode(buffer);
assertMetaDataSame(original1, decoded1);
assertEquals(2, encoder.getHpackContext().size());
assertEquals(2, decoder.getHpackContext().size());
assertEquals("x", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 0).getHttpField().getName());
assertEquals("x", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length).getHttpField().getName());
assertEquals("foo", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 1).getHttpField().getName());
}
private void assertMetadataSame(MetaData.Response expected, MetaData.Response actual)
@Test
public void testHopHeadersAreRemoved() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096, 16384);
HttpFields input = new HttpFields();
input.put(HttpHeader.ACCEPT, "*");
input.put(HttpHeader.CONNECTION, "TE, Upgrade, Custom");
input.put("Custom", "Pizza");
input.put(HttpHeader.KEEP_ALIVE, "true");
input.put(HttpHeader.PROXY_CONNECTION, "foo");
input.put(HttpHeader.TE, "1234567890abcdef");
input.put(HttpHeader.TRANSFER_ENCODING, "chunked");
input.put(HttpHeader.UPGRADE, "gold");
ByteBuffer buffer = BufferUtil.allocate(2048);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, input));
BufferUtil.flipToFlush(buffer, 0);
MetaData metaData = decoder.decode(buffer);
HttpFields output = metaData.getFields();
assertEquals(1, output.size());
assertEquals("*", output.get(HttpHeader.ACCEPT));
}
@Test
public void testTETrailers() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096, 16384);
HttpFields input = new HttpFields();
input.put(HttpHeader.CONNECTION, "TE");
String teValue = "trailers";
input.put(HttpHeader.TE, teValue);
String trailerValue = "Custom";
input.put(HttpHeader.TRAILER, trailerValue);
ByteBuffer buffer = BufferUtil.allocate(2048);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, input));
BufferUtil.flipToFlush(buffer, 0);
MetaData metaData = decoder.decode(buffer);
HttpFields output = metaData.getFields();
assertEquals(2, output.size());
assertEquals(teValue, output.get(HttpHeader.TE));
assertEquals(trailerValue, output.get(HttpHeader.TRAILER));
}
private void assertMetaDataResponseSame(MetaData.Response expected, MetaData.Response actual)
{
assertThat("Response.status", actual.getStatus(), is(expected.getStatus()));
assertThat("Response.reason", actual.getReason(), is(expected.getReason()));
assertMetadataSame((MetaData)expected, (MetaData)actual);
assertMetaDataSame(expected, actual);
}
private void assertMetadataSame(MetaData expected, MetaData actual)
private void assertMetaDataSame(MetaData expected, MetaData actual)
{
assertThat("Metadata.contentLength", actual.getContentLength(), is(expected.getContentLength()));
assertThat("Metadata.version" + ".version", actual.getHttpVersion(), is(expected.getHttpVersion()));
assertHttpFieldsSame("Metadata.fields", expected.getFields(), actual.getFields());
assertHttpFieldsSame(expected.getFields(), actual.getFields());
}
private void assertHttpFieldsSame(String msg, HttpFields expected, HttpFields actual)
private void assertHttpFieldsSame(HttpFields expected, HttpFields actual)
{
assertThat(msg + ".size", actual.size(), is(expected.size()));
assertThat("metaData.fields.size", actual.size(), is(expected.size()));
for (HttpField actualField : actual)
{
@ -203,7 +277,7 @@ public class HpackTest
// during testing.
continue;
}
assertThat(msg + ".contains(" + actualField + ")", expected.contains(actualField), is(true));
assertThat("metaData.fields.contains(" + actualField + ")", expected.contains(actualField), is(true));
}
}
}

View File

@ -25,11 +25,13 @@ import java.util.stream.Stream;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.hamcrest.Matchers;
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.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ -77,8 +79,7 @@ public class HuffmanTest
{
String s = "bad '" + bad + "'";
assertThrows(IllegalArgumentException.class,
() -> Huffman.octetsNeeded(s));
assertThat(Huffman.octetsNeeded(s), Matchers.is(-1));
assertThrows(BufferOverflowException.class,
() -> Huffman.encode(BufferUtil.allocate(32), s));

64
jetty-openid/pom.xml Normal file
View File

@ -0,0 +1,64 @@
<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.21-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-openid</artifactId>
<name>Jetty :: OpenID</name>
<description>Jetty OpenID Connect infrastructure</description>
<url>http://www.eclipse.org/jetty</url>
<properties>
<bundle-symbolic-name>${project.groupId}.openid</bundle-symbolic-name>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>findbugs-maven-plugin</artifactId>
<configuration>
<onlyAnalyze>org.eclipse.jetty.security.openid.*</onlyAnalyze>
</configuration>
</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-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util-ajax</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</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-client</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,29 @@
<?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="OpenIdConfiguration" class="org.eclipse.jetty.security.openid.OpenIdConfiguration">
<Arg><Property name="jetty.openid.openIdProvider"/></Arg>
<Arg><Property name="jetty.openid.clientId"/></Arg>
<Arg><Property name="jetty.openid.clientSecret"/></Arg>
<Call name="addScopes">
<Arg>
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg><Property name="jetty.openid.scopes"/></Arg>
</Call>
</Arg>
</Call>
</New>
<Call name="addBean">
<Arg>
<New class="org.eclipse.jetty.security.openid.OpenIdLoginService">
<Arg><Ref refid="OpenIdConfiguration"/></Arg>
<Arg><Ref refid="BaseLoginService"/></Arg>
<Call name="authenticateNewUsers">
<Arg type="boolean">
<Property name="jetty.openid.authenticateNewUsers" default="false"/>
</Arg>
</Call>
</New>
</Arg>
</Call>
</Configure>

View File

@ -0,0 +1,34 @@
DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[description]
Adds OpenId Connect authentication.
[depend]
security
[lib]
lib/jetty-openid-${jetty.version}.jar
lib/jetty-util-ajax-${jetty.version}.jar
[files]
basehome:modules/openid/openid-baseloginservice.xml|etc/openid-baseloginservice.xml
[xml]
etc/openid-baseloginservice.xml
etc/jetty-openid.xml
[ini-template]
## The OpenID Identity Provider
# jetty.openid.openIdProvider=https://accounts.google.com/
## The Client Identifier
# jetty.openid.clientId=test1234.apps.googleusercontent.com
## The Client Secret
# jetty.openid.clientSecret=XT_Mafv_aUCGheuCaKY8P
## Additional Scopes to Request
# jetty.openid.scopes=email,profile
## Whether to Authenticate users not found by base LoginService
# jetty.openid.authenticateNewUsers=false

View File

@ -0,0 +1,10 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="BaseLoginService">
<!-- Optional code to configure the base LoginService used by the OpenIdLoginService
<New id="BaseLoginService" class="org.eclipse.jetty.security.HashLoginService">
<Set name="config"><SystemProperty name="jetty.home" default="."/>/etc/realm.properties</Set>
<Set name="hotReload">true</Set>
</New>
-->
</Configure>

View File

@ -0,0 +1,491 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// 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.openid;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
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.HttpMethod;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.security.authentication.DeferredAuthentication;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.security.authentication.SessionAuthentication;
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.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.security.Constraint;
/**
* <p>Implements authentication using OpenId Connect on top of OAuth 2.0.
*
* <p>The OpenIdAuthenticator redirects unauthenticated requests to the OpenID Connect Provider. The End-User is
* eventually redirected back with an Authorization Code to the /j_security_check URI within the context.
* The Authorization Code is then used to authenticate the user through the {@link OpenIdCredentials} and {@link OpenIdLoginService}.
* </p>
* <p>
* Once a user is authenticated the OpenID Claims can be retrieved through an attribute on the session with the key {@link #CLAIMS}.
* The full response containing the OAuth 2.0 Access Token can be obtained with the session attribute {@link #RESPONSE}.
* </p>
* <p>{@link SessionAuthentication} is then used to wrap Authentication results so that they are associated with the session.</p>
*/
public class OpenIdAuthenticator extends LoginAuthenticator
{
private static final Logger LOG = Log.getLogger(OpenIdAuthenticator.class);
public static final String CLAIMS = "org.eclipse.jetty.security.openid.claims";
public static final String RESPONSE = "org.eclipse.jetty.security.openid.response";
public static final String ERROR_PAGE = "org.eclipse.jetty.security.openid.error_page";
public static final String J_URI = "org.eclipse.jetty.security.openid.URI";
public static final String J_POST = "org.eclipse.jetty.security.openid.POST";
public static final String J_METHOD = "org.eclipse.jetty.security.openid.METHOD";
public static final String CSRF_TOKEN = "org.eclipse.jetty.security.openid.csrf_token";
public static final String J_SECURITY_CHECK = "/j_security_check";
private OpenIdConfiguration _configuration;
private String _errorPage;
private String _errorPath;
private boolean _alwaysSaveUri;
public OpenIdAuthenticator()
{
}
public OpenIdAuthenticator(OpenIdConfiguration configuration, String errorPage)
{
this._configuration = configuration;
if (errorPage != null)
setErrorPage(errorPage);
}
@Override
public void setConfiguration(AuthConfiguration configuration)
{
super.setConfiguration(configuration);
String error = configuration.getInitParameter(ERROR_PAGE);
if (error != null)
setErrorPage(error);
if (_configuration != null)
return;
LoginService loginService = configuration.getLoginService();
if (!(loginService instanceof OpenIdLoginService))
throw new IllegalArgumentException("invalid LoginService");
this._configuration = ((OpenIdLoginService)loginService).getConfiguration();
}
@Override
public String getAuthMethod()
{
return Constraint.__OPENID_AUTH;
}
/**
* 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.
*
* @param alwaysSave true to always save the uri
*/
public void setAlwaysSaveUri(boolean alwaysSave)
{
_alwaysSaveUri = alwaysSave;
}
public boolean isAlwaysSaveUri()
{
return _alwaysSaveUri;
}
private void setErrorPage(String path)
{
if (path == null || path.trim().length() == 0)
{
_errorPath = null;
_errorPage = null;
}
else
{
if (!path.startsWith("/"))
{
LOG.warn("error-page must start with /");
path = "/" + path;
}
_errorPage = path;
_errorPath = path;
if (_errorPath.indexOf('?') > 0)
_errorPath = _errorPath.substring(0, _errorPath.indexOf('?'));
}
}
@Override
public UserIdentity login(String username, Object credentials, ServletRequest request)
{
if (LOG.isDebugEnabled())
LOG.debug("login {} {} {}", username, credentials, request);
UserIdentity user = super.login(username, credentials, request);
if (user != null)
{
HttpSession session = ((HttpServletRequest)request).getSession();
Authentication cached = new SessionAuthentication(getAuthMethod(), user, credentials);
session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached);
session.setAttribute(CLAIMS, ((OpenIdCredentials)credentials).getClaims());
session.setAttribute(RESPONSE, ((OpenIdCredentials)credentials).getResponse());
}
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);
session.removeAttribute(CLAIMS);
session.removeAttribute(RESPONSE);
}
@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
{
final HttpServletRequest request = (HttpServletRequest)req;
final HttpServletResponse response = (HttpServletResponse)res;
final Request baseRequest = Request.getBaseRequest(request);
final Response baseResponse = baseRequest.getResponse();
String uri = request.getRequestURI();
if (uri == null)
uri = URIUtil.SLASH;
mandatory |= isJSecurityCheck(uri);
if (!mandatory)
return new DeferredAuthentication(this);
if (isErrorPage(URIUtil.addPaths(request.getServletPath(), request.getPathInfo())) && !DeferredAuthentication.isDeferred(response))
return new DeferredAuthentication(this);
try
{
// Handle a request for authentication.
if (isJSecurityCheck(uri))
{
String authCode = request.getParameter("code");
if (authCode != null)
{
// Verify anti-forgery state token
String state = request.getParameter("state");
String antiForgeryToken = (String)request.getSession().getAttribute(CSRF_TOKEN);
if (antiForgeryToken == null || !antiForgeryToken.equals(state))
{
LOG.warn("auth failed 403: invalid state parameter");
if (response != null)
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return Authentication.SEND_FAILURE;
}
// Attempt to login with the provided authCode
OpenIdCredentials credentials = new OpenIdCredentials(authCode, getRedirectUri(request), _configuration);
UserIdentity user = login(null, credentials, request);
HttpSession session = request.getSession(false);
if (user != null)
{
// Redirect to original request
String nuri;
synchronized (session)
{
nuri = (String)session.getAttribute(J_URI);
if (nuri == null || nuri.length() == 0)
{
nuri = request.getContextPath();
if (nuri.length() == 0)
nuri = URIUtil.SLASH;
}
}
OpenIdAuthentication openIdAuth = new OpenIdAuthentication(getAuthMethod(), user);
if (LOG.isDebugEnabled())
LOG.debug("authenticated {}->{}", openIdAuth, nuri);
response.setContentLength(0);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(nuri));
return openIdAuth;
}
}
// not authenticated
if (LOG.isDebugEnabled())
LOG.debug("OpenId authentication FAILED");
if (_errorPage == null)
{
if (LOG.isDebugEnabled())
LOG.debug("auth failed 403");
if (response != null)
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("auth failed {}", _errorPage);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), _errorPage)));
}
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()))
{
if (LOG.isDebugEnabled())
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
if (LOG.isDebugEnabled())
LOG.debug("auth retry {}->{}", authentication, jUri);
StringBuffer buf = request.getRequestURL();
if (request.getQueryString() != null)
buf.append("?").append(request.getQueryString());
if (jUri.equals(buf.toString()))
{
@SuppressWarnings("unchecked")
MultiMap<String> jPost = (MultiMap<String>)session.getAttribute(J_POST);
if (jPost != null)
{
if (LOG.isDebugEnabled())
LOG.debug("auth rePOST {}->{}", authentication, jUri);
baseRequest.setContentParameters(jPost);
}
session.removeAttribute(J_URI);
session.removeAttribute(J_METHOD);
session.removeAttribute(J_POST);
}
}
}
if (LOG.isDebugEnabled())
LOG.debug("auth {}", authentication);
return authentication;
}
}
// if we can't send challenge
if (DeferredAuthentication.isDeferred(response))
{
if (LOG.isDebugEnabled())
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 redirect
if (session.getAttribute(J_URI) == null || isAlwaysSaveUri())
{
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
String challengeUri = getChallengeUri(request);
if (LOG.isDebugEnabled())
LOG.debug("challenge {}->{}", session.getId(), challengeUri);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(challengeUri));
return Authentication.SEND_CONTINUE;
}
catch (IOException 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 isErrorPage(String pathInContext)
{
return pathInContext != null && (pathInContext.equals(_errorPath));
}
private String getRedirectUri(HttpServletRequest request)
{
final StringBuffer redirectUri = new StringBuffer(128);
URIUtil.appendSchemeHostPort(redirectUri, request.getScheme(),
request.getServerName(), request.getServerPort());
redirectUri.append(request.getContextPath());
redirectUri.append(J_SECURITY_CHECK);
return redirectUri.toString();
}
protected String getChallengeUri(HttpServletRequest request)
{
HttpSession session = request.getSession();
String antiForgeryToken;
synchronized (session)
{
antiForgeryToken = (session.getAttribute(CSRF_TOKEN) == null)
? new BigInteger(130, new SecureRandom()).toString(32)
: (String)session.getAttribute(CSRF_TOKEN);
session.setAttribute(CSRF_TOKEN, antiForgeryToken);
}
// any custom scopes requested from configuration
StringBuilder scopes = new StringBuilder();
for (String s : _configuration.getScopes())
{
scopes.append(" ").append(s);
}
return _configuration.getAuthEndpoint() +
"?client_id=" + UrlEncoded.encodeString(_configuration.getClientId(), StandardCharsets.UTF_8) +
"&redirect_uri=" + UrlEncoded.encodeString(getRedirectUri(request), StandardCharsets.UTF_8) +
"&scope=openid" + UrlEncoded.encodeString(scopes.toString(), StandardCharsets.UTF_8) +
"&state=" + antiForgeryToken +
"&response_type=code";
}
@Override
public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser)
{
return true;
}
/**
* This Authentication represents a just completed OpenId Connect authentication.
* Subsequent requests from the same user are authenticated by the presents
* of a {@link SessionAuthentication} instance in their session.
*/
public static class OpenIdAuthentication extends UserAuthentication implements Authentication.ResponseSent
{
public OpenIdAuthentication(String method, UserIdentity userIdentity)
{
super(method, userIdentity);
}
@Override
public String toString()
{
return "OpenId" + super.toString();
}
}
}

View File

@ -0,0 +1,40 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// 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.openid;
import javax.servlet.ServletContext;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.DefaultAuthenticatorFactory;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.security.Constraint;
public class OpenIdAuthenticatorFactory extends DefaultAuthenticatorFactory
{
@Override
public Authenticator getAuthenticator(Server server, ServletContext context, Authenticator.AuthConfiguration configuration, IdentityService identityService, LoginService loginService)
{
String auth = configuration.getAuthMethod();
if (Constraint.__OPENID_AUTH.equalsIgnoreCase(auth))
return new OpenIdAuthenticator();
return super.getAuthenticator(server, context, configuration, identityService, loginService);
}
}

View File

@ -0,0 +1,141 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// 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.openid;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.ajax.JSON;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Holds the configuration for an OpenID Connect service.
*
* This uses the OpenID Provider URL with the path {@link #CONFIG_PATH} to discover
* the required information about the OIDC service.
*/
public class OpenIdConfiguration implements Serializable
{
private static final Logger LOG = Log.getLogger(OpenIdConfiguration.class);
private static final long serialVersionUID = 2227941990601349102L;
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
private final String openIdProvider;
private final String issuer;
private final String authEndpoint;
private final String tokenEndpoint;
private final String clientId;
private final String clientSecret;
private final Map<String, Object> discoveryDocument;
private final List<String> scopes = new ArrayList<>();
/**
* Create an OpenID configuration for a specific OIDC provider.
* @param provider The URL of the OpenID provider.
* @param clientId OAuth 2.0 Client Identifier valid at the Authorization Server.
* @param clientSecret The client secret known only by the Client and the Authorization Server.
*/
public OpenIdConfiguration(String provider, String clientId, String clientSecret)
{
this.openIdProvider = provider;
this.clientId = clientId;
this.clientSecret = clientSecret;
try
{
if (provider.endsWith("/"))
provider = provider.substring(0, provider.length() - 1);
URI providerUri = URI.create(provider + CONFIG_PATH);
InputStream inputStream = providerUri.toURL().openConnection().getInputStream();
String content = IO.toString(inputStream);
discoveryDocument = (Map)JSON.parse(content);
if (LOG.isDebugEnabled())
LOG.debug("discovery document {}", discoveryDocument);
}
catch (Throwable e)
{
throw new IllegalArgumentException("invalid identity provider", e);
}
issuer = (String)discoveryDocument.get("issuer");
if (issuer == null)
throw new IllegalArgumentException();
authEndpoint = (String)discoveryDocument.get("authorization_endpoint");
if (authEndpoint == null)
throw new IllegalArgumentException("authorization_endpoint");
tokenEndpoint = (String)discoveryDocument.get("token_endpoint");
if (tokenEndpoint == null)
throw new IllegalArgumentException("token_endpoint");
}
public Map<String, Object> getDiscoveryDocument()
{
return discoveryDocument;
}
public String getAuthEndpoint()
{
return authEndpoint;
}
public String getClientId()
{
return clientId;
}
public String getClientSecret()
{
return clientSecret;
}
public String getIssuer()
{
return issuer;
}
public String getOpenIdProvider()
{
return openIdProvider;
}
public String getTokenEndpoint()
{
return tokenEndpoint;
}
public void addScopes(String... scopes)
{
Collections.addAll(this.scopes, scopes);
}
public List<String> getScopes()
{
return scopes;
}
}

View File

@ -0,0 +1,214 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// 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.openid;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.ajax.JSON;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* <p>The credentials of an user to be authenticated with OpenID Connect. This will contain
* the OpenID ID Token and the OAuth 2.0 Access Token.</p>
*
* <p>
* This is constructed with an authorization code from the authentication request. This authorization code
* is then exchanged using {@link #redeemAuthCode()} for a response containing the ID Token and Access Token.
* The response is then validated against the {@link OpenIdConfiguration}.
* </p>
*/
public class OpenIdCredentials implements Serializable
{
private static final Logger LOG = Log.getLogger(OpenIdCredentials.class);
private static final long serialVersionUID = 4766053233370044796L;
private final String redirectUri;
private final OpenIdConfiguration configuration;
private String authCode;
private Map<String, Object> response;
private Map<String, Object> claims;
public OpenIdCredentials(String authCode, String redirectUri, OpenIdConfiguration configuration)
{
this.authCode = authCode;
this.redirectUri = redirectUri;
this.configuration = configuration;
}
public String getUserId()
{
return (String)claims.get("sub");
}
public Map<String, Object> getClaims()
{
return claims;
}
public Map<String, Object> getResponse()
{
return response;
}
public void redeemAuthCode() throws IOException
{
if (LOG.isDebugEnabled())
LOG.debug("redeemAuthCode() {}", this);
if (authCode != null)
{
try
{
response = claimAuthCode(authCode);
if (LOG.isDebugEnabled())
LOG.debug("response: {}", response);
String idToken = (String)response.get("id_token");
if (idToken == null)
throw new IllegalArgumentException("no id_token");
String accessToken = (String)response.get("access_token");
if (accessToken == null)
throw new IllegalArgumentException("no access_token");
String tokenType = (String)response.get("token_type");
if (!"Bearer".equalsIgnoreCase(tokenType))
throw new IllegalArgumentException("invalid token_type");
claims = decodeJWT(idToken);
if (LOG.isDebugEnabled())
LOG.debug("claims {}", claims);
validateClaims();
}
finally
{
// reset authCode as it can only be used once
authCode = null;
}
}
}
private void validateClaims()
{
// Issuer Identifier for the OpenID Provider MUST exactly match the value of the iss (issuer) Claim.
if (!configuration.getIssuer().equals(claims.get("iss")))
throw new IllegalArgumentException("Issuer Identifier MUST exactly match the iss Claim");
// The aud (audience) Claim MUST contain the client_id value.
if (!configuration.getClientId().equals(claims.get("aud")))
throw new IllegalArgumentException("Audience Claim MUST contain the client_id value");
// If an azp (authorized party) Claim is present, verify that its client_id is the Claim Value.
Object azp = claims.get("azp");
if (azp != null && !configuration.getClientId().equals(azp))
throw new IllegalArgumentException("Authorized party claim value should be the client_id");
}
public boolean isExpired()
{
if (authCode != null || claims == null)
return true;
// Check expiry
long expiry = (Long)claims.get("exp");
long currentTimeSeconds = (long)(System.currentTimeMillis() / 1000F);
if (currentTimeSeconds > expiry)
{
if (LOG.isDebugEnabled())
LOG.debug("OpenId Credentials expired {}", this);
return true;
}
return false;
}
protected Map<String, Object> decodeJWT(String jwt) throws IOException
{
if (LOG.isDebugEnabled())
LOG.debug("decodeJWT {}", jwt);
String[] sections = jwt.split("\\.");
if (sections.length != 3)
throw new IllegalArgumentException("JWT does not contain 3 sections");
Base64.Decoder decoder = Base64.getDecoder();
String jwtHeaderString = new String(decoder.decode(sections[0]), StandardCharsets.UTF_8);
String jwtClaimString = new String(decoder.decode(sections[1]), StandardCharsets.UTF_8);
String jwtSignature = sections[2];
Map<String, Object> jwtHeader = (Map)JSON.parse(jwtHeaderString);
LOG.debug("JWT Header: {}", jwtHeader);
/* If the ID Token is received via direct communication between the Client
and the Token Endpoint (which it is in this flow), the TLS server validation
MAY be used to validate the issuer in place of checking the token signature. */
if (LOG.isDebugEnabled())
LOG.debug("JWT signature not validated {}", jwtSignature);
return (Map)JSON.parse(jwtClaimString);
}
private Map<String, Object> claimAuthCode(String authCode) throws IOException
{
if (LOG.isDebugEnabled())
LOG.debug("claimAuthCode {}", authCode);
// Use the authorization code to get the id_token from the OpenID Provider
String urlParameters = "code=" + authCode +
"&client_id=" + UrlEncoded.encodeString(configuration.getClientId(), StandardCharsets.UTF_8) +
"&client_secret=" + UrlEncoded.encodeString(configuration.getClientSecret(), StandardCharsets.UTF_8) +
"&redirect_uri=" + UrlEncoded.encodeString(redirectUri, StandardCharsets.UTF_8) +
"&grant_type=authorization_code";
URL url = new URL(configuration.getTokenEndpoint());
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
try
{
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Host", configuration.getOpenIdProvider());
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream()))
{
wr.write(urlParameters.getBytes(StandardCharsets.UTF_8));
}
try (InputStream content = (InputStream)connection.getContent())
{
return (Map)JSON.parse(IO.toString(content));
}
}
finally
{
connection.disconnect();
}
}
}

View File

@ -0,0 +1,170 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// 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.openid;
import java.security.Principal;
import javax.security.auth.Subject;
import javax.servlet.ServletRequest;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
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;
/**
* The implementation of {@link LoginService} required to use OpenID Connect.
*
* <p>
* Can contain an optional wrapped {@link LoginService} which is used to store role information about users.
* </p>
*/
public class OpenIdLoginService extends ContainerLifeCycle implements LoginService
{
private static final Logger LOG = Log.getLogger(OpenIdLoginService.class);
private final OpenIdConfiguration _configuration;
private final LoginService loginService;
private IdentityService identityService;
private boolean authenticateNewUsers;
public OpenIdLoginService(OpenIdConfiguration configuration)
{
this(configuration, null);
}
/**
* Use a wrapped {@link LoginService} to store information about user roles.
* Users in the wrapped loginService must be stored with their username as
* the value of the sub (subject) Claim, and a credentials value of the empty string.
* @param configuration the OpenID configuration to use.
* @param loginService the wrapped LoginService to defer to for user roles.
*/
public OpenIdLoginService(OpenIdConfiguration configuration, LoginService loginService)
{
_configuration = configuration;
this.loginService = loginService;
addBean(this.loginService);
}
@Override
public String getName()
{
return _configuration.getOpenIdProvider();
}
public OpenIdConfiguration getConfiguration()
{
return _configuration;
}
@Override
public UserIdentity login(String identifier, Object credentials, ServletRequest req)
{
if (LOG.isDebugEnabled())
LOG.debug("login({}, {}, {})", identifier, credentials, req);
OpenIdCredentials openIdCredentials = (OpenIdCredentials)credentials;
try
{
openIdCredentials.redeemAuthCode();
if (openIdCredentials.isExpired())
return null;
}
catch (Throwable e)
{
LOG.warn(e);
return null;
}
OpenIdUserPrincipal userPrincipal = new OpenIdUserPrincipal(openIdCredentials);
Subject subject = new Subject();
subject.getPrincipals().add(userPrincipal);
subject.getPrivateCredentials().add(credentials);
subject.setReadOnly();
if (loginService != null)
{
UserIdentity userIdentity = loginService.login(openIdCredentials.getUserId(), "", req);
if (userIdentity == null)
{
if (isAuthenticateNewUsers())
return getIdentityService().newUserIdentity(subject, userPrincipal, new String[0]);
return null;
}
return new OpenIdUserIdentity(subject, userPrincipal, userIdentity);
}
return identityService.newUserIdentity(subject, userPrincipal, new String[0]);
}
public boolean isAuthenticateNewUsers()
{
return authenticateNewUsers;
}
/**
* This setting is only meaningful if a wrapped {@link LoginService} has been set.
* <p>
* If set to true, any users not found by the wrapped {@link LoginService} will still
* be authenticated but with no roles, if set to false users will not be
* authenticated unless they are discovered by the wrapped {@link LoginService}.
* </p>
* @param authenticateNewUsers whether to authenticate users not found by a wrapping LoginService
*/
public void setAuthenticateNewUsers(boolean authenticateNewUsers)
{
this.authenticateNewUsers = authenticateNewUsers;
}
@Override
public boolean validate(UserIdentity user)
{
Principal userPrincipal = user.getUserPrincipal();
if (!(userPrincipal instanceof OpenIdUserPrincipal))
return false;
OpenIdCredentials credentials = ((OpenIdUserPrincipal)userPrincipal).getCredentials();
return !credentials.isExpired();
}
@Override
public IdentityService getIdentityService()
{
return loginService == null ? identityService : loginService.getIdentityService();
}
@Override
public void setIdentityService(IdentityService service)
{
if (isRunning())
throw new IllegalStateException("Running");
if (loginService != null)
loginService.setIdentityService(service);
else
identityService = service;
}
@Override
public void logout(UserIdentity user)
{
}
}

View File

@ -0,0 +1,56 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// 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.openid;
import java.security.Principal;
import javax.security.auth.Subject;
import org.eclipse.jetty.server.UserIdentity;
public class OpenIdUserIdentity implements UserIdentity
{
private final Subject subject;
private final Principal userPrincipal;
private final UserIdentity userIdentity;
public OpenIdUserIdentity(Subject subject, Principal userPrincipal, UserIdentity userIdentity)
{
this.subject = subject;
this.userPrincipal = userPrincipal;
this.userIdentity = userIdentity;
}
@Override
public Subject getSubject()
{
return subject;
}
@Override
public Principal getUserPrincipal()
{
return userPrincipal;
}
@Override
public boolean isUserInRole(String role, Scope scope)
{
return userIdentity != null && userIdentity.isUserInRole(role, scope);
}
}

View File

@ -0,0 +1,50 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// 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.openid;
import java.io.Serializable;
import java.security.Principal;
public class OpenIdUserPrincipal implements Principal, Serializable
{
private static final long serialVersionUID = 1521094652756670469L;
private final OpenIdCredentials _credentials;
public OpenIdUserPrincipal(OpenIdCredentials credentials)
{
_credentials = credentials;
}
public OpenIdCredentials getCredentials()
{
return _credentials;
}
@Override
public String getName()
{
return _credentials.getUserId();
}
@Override
public String toString()
{
return _credentials.getUserId();
}
}

View File

@ -0,0 +1,226 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// 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.openid;
import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.security.Constraint;
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 OpenIdAuthenticationTest
{
public static final String CLIENT_ID = "testClient101";
public static final String CLIENT_SECRET = "secret37989798";
private OpenIdProvider openIdProvider;
private Server server;
private ServerConnector connector;
private HttpClient client;
@BeforeEach
public void setup() throws Exception
{
openIdProvider = new OpenIdProvider(CLIENT_ID, CLIENT_SECRET);
openIdProvider.start();
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
// Add servlets
context.addServlet(LoginPage.class, "/login");
context.addServlet(LogoutPage.class, "/logout");
context.addServlet(HomePage.class, "/*");
context.addServlet(ErrorPage.class, "/error");
// configure security constraints
Constraint constraint = new Constraint();
constraint.setName(Constraint.__OPENID_AUTH);
constraint.setRoles(new String[]{"**"});
constraint.setAuthenticate(true);
Constraint adminConstraint = new Constraint();
adminConstraint.setName(Constraint.__OPENID_AUTH);
adminConstraint.setRoles(new String[]{"admin"});
adminConstraint.setAuthenticate(true);
// constraint mappings
ConstraintMapping profileMapping = new ConstraintMapping();
profileMapping.setConstraint(constraint);
profileMapping.setPathSpec("/profile");
ConstraintMapping loginMapping = new ConstraintMapping();
loginMapping.setConstraint(constraint);
loginMapping.setPathSpec("/login");
ConstraintMapping adminMapping = new ConstraintMapping();
adminMapping.setConstraint(adminConstraint);
adminMapping.setPathSpec("/admin");
// security handler
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
securityHandler.setRealmName("OpenID Connect Authentication");
securityHandler.addConstraintMapping(profileMapping);
securityHandler.addConstraintMapping(loginMapping);
securityHandler.addConstraintMapping(adminMapping);
// Authentication using local OIDC Provider
OpenIdConfiguration configuration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET);
// Configure OpenIdLoginService optionally providing a base LoginService to provide user roles
OpenIdLoginService loginService = new OpenIdLoginService(configuration);//, hashLoginService);
securityHandler.setLoginService(loginService);
Authenticator authenticator = new OpenIdAuthenticator(configuration, "/error");
securityHandler.setAuthenticator(authenticator);
context.setSecurityHandler(securityHandler);
server.start();
String redirectUri = "http://localhost:"+connector.getLocalPort() + "/j_security_check";
openIdProvider.addRedirectUri(redirectUri);
client = new HttpClient();
client.start();
}
@AfterEach
public void stop() throws Exception
{
openIdProvider.stop();
server.stop();
}
@Test
public void testLoginLogout() throws Exception
{
String appUriString = "http://localhost:"+connector.getLocalPort();
// Initially not authenticated
ContentResponse response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
String[] content = response.getContentAsString().split("\n");
assertThat(content.length, is(1));
assertThat(content[0], is("not authenticated"));
// Request to login is success
response = client.GET(appUriString + "/login");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("\n");
assertThat(content.length, is(1));
assertThat(content[0], is("success"));
// Now authenticated we can get info
response = client.GET(appUriString + "/");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("\n");
assertThat(content.length, is(3));
assertThat(content[0], is("userId: 123456789"));
assertThat(content[1], is("name: FirstName LastName"));
assertThat(content[2], is("email: FirstName@fake-email.com"));
// Request to admin page gives 403 as we do not have admin role
response = client.GET(appUriString + "/admin");
assertThat(response.getStatus(), is(HttpStatus.FORBIDDEN_403));
// We are no longer authenticated after logging out
response = client.GET(appUriString + "/logout");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
content = response.getContentAsString().split("\n");
assertThat(content.length, is(1));
assertThat(content[0], is("not authenticated"));
}
public static class LoginPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.getWriter().println("success");
}
}
public static class LogoutPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
request.getSession().invalidate();
response.sendRedirect("/");
}
}
public static class AdminPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println(userInfo.get("sub") + ": success");
}
}
public static class HomePage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType("text/plain");
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null)
{
Map<String, Object> userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS);
response.getWriter().println("userId: " + userInfo.get("sub"));
response.getWriter().println("name: " + userInfo.get("name"));
response.getWriter().println("email: " + userInfo.get("email"));
}
else
{
response.getWriter().println("not authenticated");
}
}
}
public static class ErrorPage extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setContentType("text/plain");
response.getWriter().println("not authorized");
}
}
}

View File

@ -0,0 +1,254 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// 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.openid;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
public class OpenIdProvider extends ContainerLifeCycle
{
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
private static final String AUTH_PATH = "/auth";
private static final String TOKEN_PATH = "/token";
private final Map<String, User> issuedAuthCodes = new HashMap<>();
protected final String clientId;
protected final String clientSecret;
protected final List<String> redirectUris = new ArrayList<>();
private String provider;
private Server server;
private ServerConnector connector;
public OpenIdProvider(String clientId, String clientSecret)
{
this.clientId = clientId;
this.clientSecret = clientSecret;
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
ServletContextHandler contextHandler = new ServletContextHandler();
contextHandler.setContextPath("/");
contextHandler.addServlet(new ServletHolder(new OpenIdConfigServlet()), CONFIG_PATH);
contextHandler.addServlet(new ServletHolder(new OpenIdAuthEndpoint()), AUTH_PATH);
contextHandler.addServlet(new ServletHolder(new OpenIdTokenEndpoint()), TOKEN_PATH);
server.setHandler(contextHandler);
addBean(server);
}
@Override
protected void doStart() throws Exception
{
super.doStart();
provider = "http://localhost:" + connector.getLocalPort();
}
public String getProvider()
{
if (!isStarted())
throw new IllegalStateException();
return provider;
}
public void addRedirectUri(String uri)
{
redirectUris.add(uri);
}
public class OpenIdAuthEndpoint extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
if (!clientId.equals(req.getParameter("client_id")))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id");
return;
}
String redirectUri = req.getParameter("redirect_uri");
if (!redirectUris.contains(redirectUri))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri");
return;
}
String scopeString = req.getParameter("scope");
List<String> scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString));
if (!scopes.contains("openid"))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope");
return;
}
if (!"code".equals(req.getParameter("response_type")))
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code");
return;
}
String state = req.getParameter("state");
if (state == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param");
return;
}
String authCode = UUID.randomUUID().toString().replace("-", "");
User user = new User(123456789, "FirstName", "LastName");
issuedAuthCodes.put(authCode, user);
final Request baseRequest = Request.getBaseRequest(req);
final Response baseResponse = baseRequest.getResponse();
redirectUri += "?code=" + authCode + "&state=" + state;
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ?
HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, resp.encodeRedirectURL(redirectUri));
}
}
public class OpenIdTokenEndpoint extends HttpServlet
{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
String code = req.getParameter("code");
if (!clientId.equals(req.getParameter("client_id")) ||
!clientSecret.equals(req.getParameter("client_secret")) ||
!redirectUris.contains(req.getParameter("redirect_uri")) ||
!"authorization_code".equals(req.getParameter("grant_type")) ||
code == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request");
return;
}
User user = issuedAuthCodes.remove(code);
if (user == null)
{
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code");
return;
}
String jwtHeader = "{\"INFO\": \"this is not used or checked in our implementation\"}";
String jwtBody = user.getIdToken();
String jwtSignature = "we do not validate signature as we use the authorization code flow";
Base64.Encoder encoder = Base64.getEncoder();
String jwt = encoder.encodeToString(jwtHeader.getBytes()) + "." +
encoder.encodeToString(jwtBody.getBytes()) + "." +
encoder.encodeToString(jwtSignature.getBytes());
String accessToken = "ABCDEFG";
long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();
String response = "{" +
"\"access_token\": \"" + accessToken + "\"," +
"\"id_token\": \"" + jwt + "\"," +
"\"expires_in\": " + expiry + "," +
"\"token_type\": \"Bearer\"" +
"}";
resp.setContentType("text/plain");
resp.getWriter().print(response);
}
}
public class OpenIdConfigServlet extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
String discoveryDocument = "{" +
"\"issuer\": \"" + provider + "\"," +
"\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," +
"\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," +
"}";
resp.getWriter().write(discoveryDocument);
}
}
public class User
{
private long subject;
private String firstName;
private String lastName;
public User(String firstName, String lastName)
{
this(new Random().nextLong(), firstName, lastName);
}
public User(long subject, String firstName, String lastName)
{
this.subject = subject;
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName()
{
return firstName;
}
public String getLastName()
{
return lastName;
}
public String getIdToken()
{
return "{" +
"\"iss\": \"" + provider + "\"," +
"\"sub\": \"" + subject + "\"," +
"\"aud\": \"" + clientId + "\"," +
"\"exp\": " + System.currentTimeMillis() + Duration.ofMinutes(1).toMillis() + "," +
"\"name\": \"" + firstName + " " + lastName + "\"," +
"\"email\": \"" + firstName + "@fake-email.com" + "\"" +
"}";
}
}
}

View File

@ -0,0 +1,3 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
# org.eclipse.jetty.LEVEL=DEBUG
# org.eclipse.jetty.security.openid.LEVEL=DEBUG

View File

@ -893,9 +893,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
protected void stopContext() throws Exception
{
// stop all the handler hierarchy
super.doStop();
// Call the context listeners
ServletContextEvent event = new ServletContextEvent(_scontext);
Collections.reverse(_destroySerletContextListeners);
@ -911,6 +908,17 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
ex.add(x);
}
}
// stop all the handler hierarchy
try
{
super.doStop();
}
catch (Exception x)
{
ex.add(x);
}
ex.ifExceptionThrow();
}

View File

@ -81,24 +81,20 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem
public SessionData load(String id) throws Exception
{
if (!isStarted())
throw new IllegalStateException ("Not started");
throw new IllegalStateException("Not started");
final AtomicReference<SessionData> reference = new AtomicReference<SessionData>();
final AtomicReference<Exception> exception = new AtomicReference<Exception>();
Runnable r = new Runnable()
Runnable r = () ->
{
@Override
public void run()
try
{
try
{
reference.set(doLoad(id));
}
catch (Exception e)
{
exception.set(e);
}
reference.set(doLoad(id));
}
catch (Exception e)
{
exception.set(e);
}
};
@ -165,7 +161,7 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem
public Set<String> getExpired(Set<String> candidates)
{
if (!isStarted())
throw new IllegalStateException ("Not started");
throw new IllegalStateException("Not started");
try
{

View File

@ -43,11 +43,11 @@ public abstract class BaseHolder<T> extends AbstractLifeCycle implements Dumpabl
{
private static final Logger LOG = Log.getLogger(BaseHolder.class);
protected final Source _source;
protected transient Class<? extends T> _class;
protected String _className;
protected boolean _extInstance;
protected ServletHandler _servletHandler;
private final Source _source;
private Class<? extends T> _class;
private String _className;
private T _instance;
private ServletHandler _servletHandler;
protected BaseHolder(Source source)
{
@ -101,7 +101,7 @@ public abstract class BaseHolder<T> extends AbstractLifeCycle implements Dumpabl
public void doStop()
throws Exception
{
if (!_extInstance)
if (_instance == null)
_class = null;
}
@ -163,12 +163,26 @@ public abstract class BaseHolder<T> extends AbstractLifeCycle implements Dumpabl
}
}
protected synchronized void setInstance(T instance)
{
_instance = instance;
if (instance == null)
setHeldClass(null);
else
setHeldClass((Class<T>)instance.getClass());
}
protected synchronized T getInstance()
{
return _instance;
}
/**
* @return True if this holder was created for a specific instance.
*/
public boolean isInstance()
public synchronized boolean isInstance()
{
return _extInstance;
return _instance != null;
}
@Override

View File

@ -91,11 +91,10 @@ public class FilterHolder extends Holder<Filter>
{
super.doStart();
if (!javax.servlet.Filter.class
.isAssignableFrom(_class))
if (!javax.servlet.Filter.class.isAssignableFrom(getHeldClass()))
{
String msg = _class + " is not a javax.servlet.Filter";
super.stop();
String msg = getHeldClass() + " is not a javax.servlet.Filter";
doStop();
throw new IllegalStateException(msg);
}
}
@ -103,15 +102,18 @@ public class FilterHolder extends Holder<Filter>
@Override
public void initialize() throws Exception
{
if (!_initialized)
synchronized (this)
{
super.initialize();
if (_filter != null)
return;
super.initialize();
_filter = getInstance();
if (_filter == null)
{
try
{
ServletContext context = _servletHandler.getServletContext();
ServletContext context = getServletHandler().getServletContext();
_filter = (context instanceof ServletContextHandler.Context)
? context.createFilter(getHeldClass())
: getHeldClass().getDeclaredConstructor().newInstance();
@ -126,37 +128,30 @@ public class FilterHolder extends Holder<Filter>
throw ex;
}
}
_config = new Config();
if (LOG.isDebugEnabled())
LOG.debug("Filter.init {}", _filter);
_filter.init(_config);
}
_initialized = true;
}
@Override
public void doStop()
throws Exception
{
super.doStop();
_config = null;
if (_filter != null)
{
try
{
destroyInstance(_filter);
}
catch (Exception e)
finally
{
LOG.warn(e);
_filter = null;
}
}
if (!_extInstance)
_filter = null;
_config = null;
_initialized = false;
super.doStop();
}
@Override
@ -172,11 +167,7 @@ public class FilterHolder extends Holder<Filter>
public synchronized void setFilter(Filter filter)
{
_filter = filter;
_extInstance = true;
setHeldClass(filter.getClass());
if (getName() == null)
setName(filter.getClass().getName());
setInstance(filter);
}
public Filter getFilter()
@ -187,19 +178,19 @@ public class FilterHolder extends Holder<Filter>
@Override
public void dump(Appendable out, String indent) throws IOException
{
if (_initParams.isEmpty())
if (getInitParameters().isEmpty())
Dumpable.dumpObjects(out, indent, this,
_filter == null ? getHeldClass() : _filter);
else
Dumpable.dumpObjects(out, indent, this,
_filter == null ? getHeldClass() : _filter,
new DumpableCollection("initParams", _initParams.entrySet()));
new DumpableCollection("initParams", getInitParameters().entrySet()));
}
@Override
public String toString()
{
return String.format("%s@%x==%s,inst=%b,async=%b", _name, hashCode(), _className, _filter != null, isAsyncSupported());
return String.format("%s@%x==%s,inst=%b,async=%b", getName(), hashCode(), getClassName(), _filter != null, isAsyncSupported());
}
public FilterRegistration.Dynamic getRegistration()
@ -220,9 +211,9 @@ public class FilterHolder extends Holder<Filter>
mapping.setServletNames(servletNames);
mapping.setDispatcherTypes(dispatcherTypes);
if (isMatchAfter)
_servletHandler.addFilterMapping(mapping);
getServletHandler().addFilterMapping(mapping);
else
_servletHandler.prependFilterMapping(mapping);
getServletHandler().prependFilterMapping(mapping);
}
@Override
@ -234,15 +225,15 @@ public class FilterHolder extends Holder<Filter>
mapping.setPathSpecs(urlPatterns);
mapping.setDispatcherTypes(dispatcherTypes);
if (isMatchAfter)
_servletHandler.addFilterMapping(mapping);
getServletHandler().addFilterMapping(mapping);
else
_servletHandler.prependFilterMapping(mapping);
getServletHandler().prependFilterMapping(mapping);
}
@Override
public Collection<String> getServletNameMappings()
{
FilterMapping[] mappings = _servletHandler.getFilterMappings();
FilterMapping[] mappings = getServletHandler().getFilterMappings();
List<String> names = new ArrayList<String>();
for (FilterMapping mapping : mappings)
{
@ -258,7 +249,7 @@ public class FilterHolder extends Holder<Filter>
@Override
public Collection<String> getUrlPatternMappings()
{
FilterMapping[] mappings = _servletHandler.getFilterMappings();
FilterMapping[] mappings = getServletHandler().getFilterMappings();
List<String> patterns = new ArrayList<String>();
for (FilterMapping mapping : mappings)
{
@ -277,7 +268,7 @@ public class FilterHolder extends Holder<Filter>
@Override
public String getFilterName()
{
return _name;
return getName();
}
}
}

View File

@ -45,16 +45,15 @@ public abstract class Holder<T> extends BaseHolder<T>
{
private static final Logger LOG = Log.getLogger(Holder.class);
protected final Map<String, String> _initParams = new HashMap<String, String>(3);
protected String _displayName;
protected boolean _asyncSupported;
protected String _name;
protected boolean _initialized = false;
private final Map<String, String> _initParams = new HashMap<String, String>(3);
private String _displayName;
private boolean _asyncSupported;
private String _name;
protected Holder(Source source)
{
super(source);
switch (_source.getOrigin())
switch (getSource().getOrigin())
{
case JAVAX_API:
case DESCRIPTOR:
@ -98,6 +97,14 @@ public abstract class Holder<T> extends BaseHolder<T>
return _name;
}
@Override
protected synchronized void setInstance(T instance)
{
super.setInstance(instance);
if (getName() == null)
setName(String.format("%s@%x", instance.getClass().getName(), instance.hashCode()));
}
public void destroyInstance(Object instance)
throws Exception
{
@ -175,7 +182,7 @@ public abstract class Holder<T> extends BaseHolder<T>
@Override
public String toString()
{
return String.format("%s@%x==%s", _name, hashCode(), _className);
return String.format("%s@%x==%s", _name, hashCode(), getClassName());
}
protected class HolderConfig
@ -183,7 +190,7 @@ public abstract class Holder<T> extends BaseHolder<T>
public ServletContext getServletContext()
{
return _servletHandler.getServletContext();
return getServletHandler().getServletContext();
}
public String getInitParameter(String param)

View File

@ -64,52 +64,66 @@ public class ListenerHolder extends BaseHolder<EventListener>
*/
public void setListener(EventListener listener)
{
_listener = listener;
_extInstance = true;
setHeldClass(_listener.getClass());
setInstance(listener);
}
@Override
public void doStart() throws Exception
{
super.doStart();
if (!java.util.EventListener.class.isAssignableFrom(_class))
if (!java.util.EventListener.class.isAssignableFrom(getHeldClass()))
{
String msg = _class + " is not a java.util.EventListener";
String msg = getHeldClass() + " is not a java.util.EventListener";
super.stop();
throw new IllegalStateException(msg);
}
ContextHandler contextHandler = ContextHandler.getCurrentContext().getContextHandler();
if (_listener == null)
if (contextHandler != null)
{
//create an instance of the listener and decorate it
try
_listener = getInstance();
if (_listener == null)
{
ServletContext scontext = contextHandler.getServletContext();
_listener = (scontext instanceof ServletContextHandler.Context)
? scontext.createListener(getHeldClass())
: getHeldClass().getDeclaredConstructor().newInstance();
}
catch (ServletException ex)
{
Throwable cause = ex.getRootCause();
if (cause instanceof InstantiationException)
throw (InstantiationException)cause;
if (cause instanceof IllegalAccessException)
throw (IllegalAccessException)cause;
throw ex;
//create an instance of the listener and decorate it
try
{
ServletContext scontext = contextHandler.getServletContext();
_listener = (scontext instanceof ServletContextHandler.Context)
? scontext.createListener(getHeldClass())
: getHeldClass().getDeclaredConstructor().newInstance();
}
catch (ServletException ex)
{
Throwable cause = ex.getRootCause();
if (cause instanceof InstantiationException)
throw (InstantiationException)cause;
if (cause instanceof IllegalAccessException)
throw (IllegalAccessException)cause;
throw ex;
}
}
contextHandler.addEventListener(_listener);
}
contextHandler.addEventListener(_listener);
}
@Override
public void doStop() throws Exception
{
super.doStop();
if (!_extInstance)
_listener = null;
if (_listener != null)
{
try
{
ContextHandler contextHandler = ContextHandler.getCurrentContext().getContextHandler();
if (contextHandler != null)
contextHandler.removeEventListener(_listener);
getServletHandler().destroyListener(_listener);
}
finally
{
_listener = null;
}
}
}
@Override

View File

@ -733,6 +733,11 @@ public class ServletContextHandler extends ContextHandler
_objFactory.destroy(filter);
}
void destroyListener(EventListener listener)
{
_objFactory.destroy(listener);
}
public static ServletContextHandler getServletContextHandler(ServletContext context)
{
ContextHandler handler = getContextHandler(context);
@ -1286,6 +1291,11 @@ public class ServletContextHandler extends ContextHandler
}
}
public <T extends Filter> void destroyFilter(T f)
{
_objFactory.destroy(f);
}
@Override
public <T extends Servlet> T createServlet(Class<T> c) throws ServletException
{
@ -1301,6 +1311,11 @@ public class ServletContextHandler extends ContextHandler
}
}
public <T extends Servlet> void destroyServlet(T s)
{
_objFactory.destroy(s);
}
@Override
public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
{

View File

@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.EventListener;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
@ -1736,6 +1737,12 @@ public class ServletHandler extends ScopedHandler
_contextHandler.destroyFilter(filter);
}
void destroyListener(EventListener listener)
{
if (_contextHandler != null)
_contextHandler.destroyListener(listener);
}
@SuppressWarnings("serial")
public static class Default404Servlet extends HttpServlet
{

View File

@ -32,6 +32,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.TimeUnit;
import javax.servlet.GenericServlet;
import javax.servlet.MultipartConfigElement;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
@ -43,6 +45,7 @@ import javax.servlet.ServletResponse;
import javax.servlet.ServletSecurityElement;
import javax.servlet.SingleThreadModel;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.RunAsToken;
@ -70,7 +73,6 @@ import org.eclipse.jetty.util.log.Logger;
@ManagedObject("Servlet Holder")
public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope, Comparable<ServletHolder>
{
private static final Logger LOG = Log.getLogger(ServletHolder.class);
private int _initOrder = -1;
private boolean _initOnStartup = false;
@ -82,11 +84,9 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
private ServletRegistration.Dynamic _registration;
private JspContainer _jspContainer;
private Servlet _servlet;
private long _unavailable;
private volatile Servlet _servlet;
private Config _config;
private boolean _enabled = true;
private UnavailableException _unavailableEx;
public static final String APACHE_SENTINEL_CLASS = "org.apache.tomcat.InstanceManager";
public static final String JSP_GENERATED_PACKAGE_NAME = "org.eclipse.jetty.servlet.jspPackagePrefix";
@ -167,7 +167,10 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
*/
public UnavailableException getUnavailableException()
{
return _unavailableEx;
Servlet servlet = _servlet;
if (servlet instanceof UnavailableServlet)
return ((UnavailableServlet)servlet).getUnavailableException();
return null;
}
public synchronized void setServlet(Servlet servlet)
@ -175,11 +178,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
if (servlet == null || servlet instanceof SingleThreadModel)
throw new IllegalArgumentException();
_extInstance = true;
_servlet = servlet;
setHeldClass(servlet.getClass());
if (getName() == null)
setName(servlet.getClass().getName() + "-" + super.hashCode());
setInstance(servlet);
}
@ManagedAttribute(value = "initialization order", readonly = true)
@ -218,20 +217,20 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
if (sh._initOrder > _initOrder)
return -1;
// consider _className, need to position properly when one is configured but not the other
// consider getClassName(), need to position properly when one is configured but not the other
int c;
if (_className == null && sh._className == null)
if (getClassName() == null && sh.getClassName() == null)
c = 0;
else if (_className == null)
else if (getClassName() == null)
c = -1;
else if (sh._className == null)
else if (sh.getClassName() == null)
c = 1;
else
c = _className.compareTo(sh._className);
c = getClassName().compareTo(sh.getClassName());
// if _initOrder and _className are the same, consider the _name
// if _initOrder and getClassName() are the same, consider the getName()
if (c == 0)
c = _name.compareTo(sh._name);
c = getName().compareTo(sh.getName());
return c;
}
@ -245,7 +244,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
@Override
public int hashCode()
{
return _name == null ? System.identityHashCode(this) : _name.hashCode();
return getName() == null ? System.identityHashCode(this) : getName().hashCode();
}
/**
@ -309,7 +308,6 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
public void doStart()
throws Exception
{
_unavailable = 0;
if (!_enabled)
return;
@ -342,7 +340,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
//copy jsp init params that don't exist for this servlet
for (Map.Entry<String, String> entry : jsp.getInitParameters().entrySet())
{
if (!_initParams.containsKey(entry.getKey()))
if (!getInitParameters().containsKey(entry.getKey()))
setInitParameter(entry.getKey(), entry.getValue());
}
//jsp specific: set up the jsp-file on the JspServlet. If load-on-startup is >=0 and the jsp container supports
@ -365,7 +363,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
catch (UnavailableException ex)
{
makeUnavailable(ex);
if (_servletHandler.isStartWithUnavailable())
if (getServletHandler().isStartWithUnavailable())
{
LOG.ignore(ex);
return;
@ -382,7 +380,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
catch (UnavailableException ex)
{
makeUnavailable(ex);
if (_servletHandler.isStartWithUnavailable())
if (getServletHandler().isStartWithUnavailable())
{
LOG.ignore(ex);
return;
@ -394,15 +392,23 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
//check if we need to forcibly set load-on-startup
checkInitOnStartup();
_identityService = _servletHandler.getIdentityService();
if (_identityService != null && _runAsRole != null)
_runAsToken = _identityService.newRunAsToken(_runAsRole);
if (_runAsRole == null)
{
_identityService = null;
_runAsToken = null;
}
else
{
_identityService = getServletHandler().getIdentityService();
if (_identityService != null)
_runAsToken = _identityService.newRunAsToken(_runAsRole);
}
_config = new Config();
synchronized (this)
{
if (_class != null && javax.servlet.SingleThreadModel.class.isAssignableFrom(_class))
if (getHeldClass() != null && javax.servlet.SingleThreadModel.class.isAssignableFrom(getHeldClass()))
_servlet = new SingleThreadedWrapper();
}
}
@ -411,57 +417,37 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
public void initialize()
throws Exception
{
if (!_initialized)
synchronized (this)
{
super.initialize();
if (_extInstance || _initOnStartup)
if (_servlet == null && (_initOnStartup || isInstance()))
{
try
{
initServlet();
}
catch (Exception e)
{
if (_servletHandler.isStartWithUnavailable())
LOG.ignore(e);
else
throw e;
}
super.initialize();
initServlet();
}
}
_initialized = true;
}
@Override
public void doStop()
throws Exception
{
Object oldRunAs = null;
if (_servlet != null)
synchronized (this)
{
try
Servlet servlet = _servlet;
if (servlet != null)
{
if (_identityService != null)
oldRunAs = _identityService.setRunAs(_identityService.getSystemUserIdentity(), _runAsToken);
destroyInstance(_servlet);
}
catch (Exception e)
{
LOG.warn(e);
}
finally
{
if (_identityService != null)
_identityService.unsetRunAs(oldRunAs);
_servlet = null;
try
{
destroyInstance(servlet);
}
catch (Exception e)
{
LOG.warn(e);
}
}
_config = null;
}
if (!_extInstance)
_servlet = null;
_config = null;
_initialized = false;
}
@Override
@ -481,41 +467,24 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
* @return The servlet
* @throws ServletException if unable to init the servlet on first use
*/
public synchronized Servlet getServlet()
public Servlet getServlet()
throws ServletException
{
Servlet servlet = _servlet;
if (servlet != null && _unavailable == 0)
return servlet;
synchronized (this)
if (servlet == null)
{
// Handle previous unavailability
if (_unavailable != 0)
synchronized (this)
{
if (_unavailable < 0 || _unavailable > 0 && System.currentTimeMillis() < _unavailable)
throw _unavailableEx;
_unavailable = 0;
_unavailableEx = null;
}
servlet = _servlet;
if (servlet != null)
return servlet;
if (isRunning())
{
if (_class == null)
throw new UnavailableException("Servlet Not Initialized");
if (_unavailable != 0 || !_initOnStartup)
initServlet();
servlet = _servlet;
if (servlet == null)
throw new UnavailableException("Could not instantiate " + _class);
if (servlet == null && isRunning())
{
if (getHeldClass() != null)
initServlet();
servlet = _servlet;
}
}
return servlet;
}
return servlet;
}
/**
@ -525,13 +494,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
*/
public Servlet getServletInstance()
{
Servlet servlet = _servlet;
if (servlet != null)
return servlet;
synchronized (this)
{
return _servlet;
}
return _servlet;
}
/**
@ -542,9 +505,9 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
public void checkServletType()
throws UnavailableException
{
if (_class == null || !javax.servlet.Servlet.class.isAssignableFrom(_class))
if (getHeldClass() == null || !javax.servlet.Servlet.class.isAssignableFrom(getHeldClass()))
{
throw new UnavailableException("Servlet " + _class + " is not a javax.servlet.Servlet");
throw new UnavailableException("Servlet " + getHeldClass() + " is not a javax.servlet.Servlet");
}
}
@ -553,18 +516,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
*/
public boolean isAvailable()
{
if (isStarted() && _unavailable == 0)
return true;
try
{
getServlet();
}
catch (Exception e)
{
LOG.ignore(e);
}
return isStarted() && _unavailable == 0;
return (isStarted() && !(_servlet instanceof UnavailableServlet));
}
/**
@ -575,30 +527,19 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
*/
private void checkInitOnStartup()
{
if (_class == null)
if (getHeldClass() == null)
return;
if ((_class.getAnnotation(javax.servlet.annotation.ServletSecurity.class) != null) && !_initOnStartup)
if ((getHeldClass().getAnnotation(javax.servlet.annotation.ServletSecurity.class) != null) && !_initOnStartup)
setInitOrder(Integer.MAX_VALUE);
}
private void makeUnavailable(UnavailableException e)
private Servlet makeUnavailable(UnavailableException e)
{
if (_unavailableEx == e && _unavailable != 0)
return;
_servletHandler.getServletContext().log("unavailable", e);
_unavailableEx = e;
_unavailable = -1;
if (e.isPermanent())
_unavailable = -1;
else
synchronized (this)
{
if (_unavailableEx.getUnavailableSeconds() > 0)
_unavailable = System.currentTimeMillis() + 1000 * _unavailableEx.getUnavailableSeconds();
else
_unavailable = System.currentTimeMillis() + 5000; // TODO configure
_servlet = new UnavailableServlet(e, _servlet);
return _servlet;
}
}
@ -608,37 +549,39 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
makeUnavailable((UnavailableException)e);
else
{
ServletContext ctx = _servletHandler.getServletContext();
ServletContext ctx = getServletHandler().getServletContext();
if (ctx == null)
LOG.info("unavailable", e);
else
ctx.log("unavailable", e);
_unavailableEx = new UnavailableException(String.valueOf(e), -1)
UnavailableException unavailable = new UnavailableException(String.valueOf(e), -1)
{
{
initCause(e);
}
};
_unavailable = -1;
makeUnavailable(unavailable);
}
}
private synchronized void initServlet()
throws ServletException
{
Object oldRunAs = null;
try
{
if (_servlet == null)
_servlet = getInstance();
if (_servlet == null)
_servlet = newInstance();
if (_config == null)
_config = new Config();
// Handle run as
if (_identityService != null)
{
oldRunAs = _identityService.setRunAs(_identityService.getSystemUserIdentity(), _runAsToken);
}
if (_identityService != null && _runAsToken != null)
_servlet = new RunAsServlet(_servlet, _identityService, _runAsToken);
if (!isAsyncSupported())
_servlet = new NotAsyncServlet(_servlet);
// Handle configuring servlets that implement org.apache.jasper.servlet.JspServlet
if (isJspServlet())
@ -658,30 +601,21 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
catch (UnavailableException e)
{
makeUnavailable(e);
_servlet = null;
_config = null;
throw e;
if (getServletHandler().isStartWithUnavailable())
LOG.warn(e);
else
throw e;
}
catch (ServletException e)
{
makeUnavailable(e.getCause() == null ? e : e.getCause());
_servlet = null;
_config = null;
throw e;
}
catch (Exception e)
{
makeUnavailable(e);
_servlet = null;
_config = null;
throw new ServletException(this.toString(), e);
}
finally
{
// pop run-as role
if (_identityService != null)
_identityService.unsetRunAs(oldRunAs);
}
}
/**
@ -692,7 +626,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
ContextHandler ch = ContextHandler.getContextHandler(getServletHandler().getServletContext());
/* Set the webapp's classpath for Jasper */
ch.setAttribute("org.apache.catalina.jsp_classpath", ch.getClassPath());
ch.setAttribute("org.apache.catalina.jspgetHeldClass()path", ch.getClassPath());
/* Set up other classpath attribute */
if ("?".equals(getInitParameter("classpath")))
@ -818,56 +752,23 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
UnavailableException,
IOException
{
if (_class == null)
throw new UnavailableException("Servlet Not Initialized");
Servlet servlet = getServlet();
// Service the request
Object oldRunAs = null;
boolean suspendable = baseRequest.isAsyncSupported();
try
{
// Handle aliased path
if (_forcedPath != null)
adaptForcedPathToJspContainer(request);
// Handle run as
if (_identityService != null)
oldRunAs = _identityService.setRunAs(baseRequest.getResolvedUserIdentity(), _runAsToken);
if (baseRequest.isAsyncSupported() && !isAsyncSupported())
{
try
{
baseRequest.setAsyncSupported(false, this.toString());
servlet.service(request, response);
}
finally
{
baseRequest.setAsyncSupported(true, null);
}
}
else
servlet.service(request, response);
Servlet servlet = getServlet();
if (servlet == null)
throw new UnavailableException("Servlet Not Initialized");
servlet.service(request, response);
}
catch (UnavailableException e)
{
makeUnavailable(e);
throw _unavailableEx;
}
finally
{
// Pop run-as role.
if (_identityService != null)
_identityService.unsetRunAs(oldRunAs);
makeUnavailable(e).service(request, response);
}
}
protected boolean isJspServlet()
{
Servlet servlet = getServletInstance();
Class<?> c = servlet == null ? _class : servlet.getClass();
Class<?> c = servlet == null ? getHeldClass() : servlet.getClass();
while (c != null)
{
@ -885,11 +786,6 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
return ("org.apache.jasper.servlet.JspServlet".equals(classname));
}
private void adaptForcedPathToJspContainer(ServletRequest request)
{
//no-op for apache jsp
}
private void detectJspContainer()
{
if (_jspContainer == null)
@ -1057,7 +953,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
Set<String> clash = null;
for (String pattern : urlPatterns)
{
ServletMapping mapping = _servletHandler.getServletMapping(pattern);
ServletMapping mapping = getServletHandler().getServletMapping(pattern);
if (mapping != null)
{
//if the servlet mapping was from a default descriptor, then allow it to be overridden
@ -1078,7 +974,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
ServletMapping mapping = new ServletMapping(Source.JAVAX_API);
mapping.setServletName(ServletHolder.this.getName());
mapping.setPathSpecs(urlPatterns);
_servletHandler.addServletMapping(mapping);
getServletHandler().addServletMapping(mapping);
return Collections.emptySet();
}
@ -1086,7 +982,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
@Override
public Collection<String> getMappings()
{
ServletMapping[] mappings = _servletHandler.getServletMappings();
ServletMapping[] mappings = getServletHandler().getServletMappings();
List<String> patterns = new ArrayList<String>();
if (mappings != null)
{
@ -1140,7 +1036,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
@Override
public Set<String> setServletSecurity(ServletSecurityElement securityElement)
{
return _servletHandler.setServletSecurity(this, securityElement);
return getServletHandler().setServletSecurity(this, securityElement);
}
}
@ -1288,18 +1184,221 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
@Override
public void dump(Appendable out, String indent) throws IOException
{
if (_initParams.isEmpty())
if (getInitParameters().isEmpty())
Dumpable.dumpObjects(out, indent, this,
_servlet == null ? getHeldClass() : _servlet);
else
Dumpable.dumpObjects(out, indent, this,
_servlet == null ? getHeldClass() : _servlet,
new DumpableCollection("initParams", _initParams.entrySet()));
new DumpableCollection("initParams", getInitParameters().entrySet()));
}
@Override
public String toString()
{
return String.format("%s@%x==%s,jsp=%s,order=%d,inst=%b,async=%b", _name, hashCode(), _className, _forcedPath, _initOrder, _servlet != null, isAsyncSupported());
return String.format("%s@%x==%s,jsp=%s,order=%d,inst=%b,async=%b", getName(), hashCode(), getClassName(), _forcedPath, _initOrder, _servlet != null, isAsyncSupported());
}
private class UnavailableServlet extends GenericServlet
{
final UnavailableException _unavailableException;
final Servlet _servlet;
final long _available;
public UnavailableServlet(UnavailableException unavailableException, Servlet servlet)
{
_unavailableException = unavailableException;
if (unavailableException.isPermanent())
{
_servlet = null;
_available = -1;
if (servlet != null)
{
try
{
destroyInstance(servlet);
}
catch (Throwable th)
{
if (th != unavailableException)
unavailableException.addSuppressed(th);
}
}
}
else
{
_servlet = servlet;
_available = System.nanoTime() + TimeUnit.SECONDS.toNanos(unavailableException.getUnavailableSeconds());
}
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException
{
if (_available == -1)
((HttpServletResponse)res).sendError(HttpServletResponse.SC_NOT_FOUND);
else if (System.nanoTime() < _available)
((HttpServletResponse)res).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
else
{
synchronized (ServletHolder.this)
{
ServletHolder.this._servlet = this._servlet;
_servlet.service(req,res);
}
}
}
@Override
public void destroy()
{
if (_servlet != null)
{
try
{
destroyInstance(_servlet);
}
catch (Throwable th)
{
LOG.warn(th);
}
}
}
public UnavailableException getUnavailableException()
{
return _unavailableException;
}
}
private static class WrapperServlet implements Servlet
{
final Servlet _servlet;
public WrapperServlet(Servlet servlet)
{
_servlet = servlet;
}
@Override
public void init(ServletConfig config) throws ServletException
{
_servlet.init(config);
}
@Override
public ServletConfig getServletConfig()
{
return _servlet.getServletConfig();
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException
{
_servlet.service(req, res);
}
@Override
public String getServletInfo()
{
return _servlet.getServletInfo();
}
@Override
public void destroy()
{
_servlet.destroy();
}
@Override
public String toString()
{
return String.format("%s:%s", this.getClass().getSimpleName(), _servlet.toString());
}
}
private static class RunAsServlet extends WrapperServlet
{
final IdentityService _identityService;
final RunAsToken _runAsToken;
public RunAsServlet(Servlet servlet, IdentityService identityService, RunAsToken runAsToken)
{
super(servlet);
_identityService = identityService;
_runAsToken = runAsToken;
}
@Override
public void init(ServletConfig config) throws ServletException
{
Object oldRunAs = _identityService.setRunAs(_identityService.getSystemUserIdentity(), _runAsToken);
try
{
_servlet.init(config);
}
finally
{
_identityService.unsetRunAs(oldRunAs);
}
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException
{
Object oldRunAs = _identityService.setRunAs(_identityService.getSystemUserIdentity(), _runAsToken);
try
{
_servlet.service(req, res);
}
finally
{
_identityService.unsetRunAs(oldRunAs);
}
}
@Override
public void destroy()
{
Object oldRunAs = _identityService.setRunAs(_identityService.getSystemUserIdentity(), _runAsToken);
try
{
_servlet.destroy();
}
finally
{
_identityService.unsetRunAs(oldRunAs);
}
}
}
private static class NotAsyncServlet extends WrapperServlet
{
public NotAsyncServlet(Servlet servlet)
{
super(servlet);
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException
{
if (req.isAsyncSupported())
{
try
{
((Request)req).setAsyncSupported(false, this.toString());
_servlet.service(req, res);
}
finally
{
((Request)req).setAsyncSupported(true, null);
}
}
else
{
_servlet.service(req, res);
}
}
}
}

View File

@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType;
@ -36,6 +37,7 @@ import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -58,6 +60,7 @@ 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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ErrorPageTest
@ -67,6 +70,8 @@ public class ErrorPageTest
private StacklessLogging _stackless;
private static CountDownLatch __asyncSendErrorCompleted;
private ErrorPageErrorHandler _errorPageErrorHandler;
private static AtomicBoolean __destroyed;
private ServletContextHandler _context;
@BeforeEach
public void init() throws Exception
@ -75,25 +80,24 @@ public class ErrorPageTest
_connector = new LocalConnector(_server);
_server.addConnector(_connector);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
_context = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
_server.setHandler(context);
_server.setHandler(_context);
context.setContextPath("/");
context.addFilter(SingleDispatchFilter.class, "/*", EnumSet.allOf(DispatcherType.class));
context.addServlet(DefaultServlet.class, "/");
context.addServlet(FailServlet.class, "/fail/*");
context.addServlet(FailClosedServlet.class, "/fail-closed/*");
context.addServlet(ErrorServlet.class, "/error/*");
context.addServlet(AppServlet.class, "/app/*");
context.addServlet(LongerAppServlet.class, "/longer.app/*");
context.addServlet(SyncSendErrorServlet.class, "/sync/*");
context.addServlet(AsyncSendErrorServlet.class, "/async/*");
context.addServlet(NotEnoughServlet.class, "/notenough/*");
context.addServlet(DeleteServlet.class, "/delete/*");
context.addServlet(ErrorAndStatusServlet.class, "/error-and-status/*");
_context.setContextPath("/");
_context.addFilter(SingleDispatchFilter.class, "/*", EnumSet.allOf(DispatcherType.class));
_context.addServlet(DefaultServlet.class, "/");
_context.addServlet(FailServlet.class, "/fail/*");
_context.addServlet(FailClosedServlet.class, "/fail-closed/*");
_context.addServlet(ErrorServlet.class, "/error/*");
_context.addServlet(AppServlet.class, "/app/*");
_context.addServlet(LongerAppServlet.class, "/longer.app/*");
_context.addServlet(SyncSendErrorServlet.class, "/sync/*");
_context.addServlet(AsyncSendErrorServlet.class, "/async/*");
_context.addServlet(NotEnoughServlet.class, "/notenough/*");
_context.addServlet(UnavailableServlet.class, "/unavailable/*");
_context.addServlet(DeleteServlet.class, "/delete/*");
_context.addServlet(ErrorAndStatusServlet.class, "/error-and-status/*");
HandlerWrapper noopHandler = new HandlerWrapper()
{
@ -106,10 +110,10 @@ public class ErrorPageTest
super.handle(target, baseRequest, request, response);
}
};
context.insertHandler(noopHandler);
_context.insertHandler(noopHandler);
_errorPageErrorHandler = new ErrorPageErrorHandler();
context.setErrorHandler(_errorPageErrorHandler);
_context.setErrorHandler(_errorPageErrorHandler);
_errorPageErrorHandler.addErrorPage(595, "/error/595");
_errorPageErrorHandler.addErrorPage(597, "/sync");
_errorPageErrorHandler.addErrorPage(599, "/error/599");
@ -408,6 +412,45 @@ public class ErrorPageTest
assertThat(response, Matchers.endsWith("SomeBytes"));
}
@Test
public void testPermanentlyUnavailable() throws Exception
{
try (StacklessLogging ignore =new StacklessLogging(_context.getLogger()))
{
try (StacklessLogging ignore2 = new StacklessLogging(HttpChannel.class))
{
__destroyed = new AtomicBoolean(false);
String response = _connector.getResponse("GET /unavailable/info HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 404 "));
assertTrue(__destroyed.get());
}
}
}
@Test
public void testUnavailable() throws Exception
{
try (StacklessLogging ignore =new StacklessLogging(_context.getLogger()))
{
try (StacklessLogging ignore2 = new StacklessLogging(HttpChannel.class))
{
__destroyed = new AtomicBoolean(false);
String response = _connector.getResponse("GET /unavailable/info?for=1 HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 503 "));
assertFalse(__destroyed.get());
response = _connector.getResponse("GET /unavailable/info?ok=true HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 503 "));
assertFalse(__destroyed.get());
Thread.sleep(1500);
response = _connector.getResponse("GET /unavailable/info?ok=true HTTP/1.0\r\n\r\n");
assertThat(response, Matchers.containsString("HTTP/1.1 200 "));
assertFalse(__destroyed.get());
}
}
}
public static class AppServlet extends HttpServlet implements Servlet
{
@Override
@ -618,6 +661,35 @@ public class ErrorPageTest
}
}
public static class UnavailableServlet extends HttpServlet implements Servlet
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
String ok = request.getParameter("ok");
if (Boolean.parseBoolean(ok))
{
response.setStatus(200);
response.flushBuffer();
return;
}
String f = request.getParameter("for");
if (f == null)
throw new UnavailableException("testing permanent");
throw new UnavailableException("testing periodic", Integer.parseInt(f));
}
@Override
public void destroy()
{
if (__destroyed != null)
__destroyed.set(true);
}
}
public static class SingleDispatchFilter implements Filter
{
ConcurrentMap<Integer, Thread> dispatches = new ConcurrentHashMap<>();
@ -665,7 +737,6 @@ public class ErrorPageTest
@Override
public void destroy()
{
}
}
}

View File

@ -1,80 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.eclipse.jetty.servlet;
import java.util.Collections;
import java.util.Set;
import javax.servlet.ServletRegistration;
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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
*
*/
public class HolderTest
{
@Test
public void testInitParams() throws Exception
{
ServletHolder holder = new ServletHolder(Source.JAVAX_API);
ServletRegistration reg = holder.getRegistration();
assertThrows(IllegalArgumentException.class,() -> reg.setInitParameter(null, "foo"));
assertThrows(IllegalArgumentException.class,() -> reg.setInitParameter("foo", null));
reg.setInitParameter("foo", "bar");
assertFalse(reg.setInitParameter("foo", "foo"));
Set<String> clash = reg.setInitParameters(Collections.singletonMap("foo", "bax"));
assertTrue(clash != null && clash.size() == 1, "should be one clash");
assertThrows(IllegalArgumentException.class,() -> reg.setInitParameters(Collections.singletonMap((String)null, "bax")));
assertThrows(IllegalArgumentException.class,() -> reg.setInitParameters(Collections.singletonMap("foo", (String)null)));
Set<String> clash2 = reg.setInitParameters(Collections.singletonMap("FOO", "bax"));
assertTrue(clash2.isEmpty(), "should be no clash");
assertEquals(2, reg.getInitParameters().size(), "setInitParameters should not replace existing non-clashing init parameters");
}
}

View File

@ -18,11 +18,11 @@
package org.eclipse.jetty.servlet;
import javax.servlet.ServletException;
import java.util.Collections;
import java.util.Set;
import javax.servlet.ServletRegistration;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.MultiException;
@ -32,18 +32,41 @@ 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.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;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.IOException;
public class ServletHolderTest
{
public static class FakeServlet extends HttpServlet
{
}
@Test
public void testInitParams() throws Exception
{
ServletHolder holder = new ServletHolder(Source.JAVAX_API);
ServletRegistration reg = holder.getRegistration();
assertThrows(IllegalArgumentException.class,() -> reg.setInitParameter(null, "foo"));
assertThrows(IllegalArgumentException.class,() -> reg.setInitParameter("foo", null));
reg.setInitParameter("foo", "bar");
assertFalse(reg.setInitParameter("foo", "foo"));
Set<String> clash = reg.setInitParameters(Collections.singletonMap("foo", "bax"));
assertTrue(clash != null && clash.size() == 1, "should be one clash");
assertThrows(IllegalArgumentException.class,() -> reg.setInitParameters(Collections.singletonMap((String)null, "bax")));
assertThrows(IllegalArgumentException.class,() -> reg.setInitParameters(Collections.singletonMap("foo", (String)null)));
Set<String> clash2 = reg.setInitParameters(Collections.singletonMap("FOO", "bax"));
assertTrue(clash2.isEmpty(), "should be no clash");
assertEquals(2, reg.getInitParameters().size(), "setInitParameters should not replace existing non-clashing init parameters");
}
@Test
public void testTransitiveCompareTo() throws Exception
@ -78,26 +101,16 @@ public class ServletHolderTest
ServletHolder h = new ServletHolder();
h.setName("test");
assertEquals(null, h.getClassNameForJsp(null));
assertEquals(null, h.getClassNameForJsp(""));
assertEquals(null, h.getClassNameForJsp("/blah/"));
assertEquals(null, h.getClassNameForJsp("//blah///"));
assertEquals(null, h.getClassNameForJsp("/a/b/c/blah/"));
assertNull(h.getClassNameForJsp(null));
assertNull(h.getClassNameForJsp(""));
assertNull(h.getClassNameForJsp("/blah/"));
assertNull(h.getClassNameForJsp("//blah///"));
assertNull(h.getClassNameForJsp("/a/b/c/blah/"));
assertEquals("org.apache.jsp.a.b.c.blah", h.getClassNameForJsp("/a/b/c/blah"));
assertEquals("org.apache.jsp.blah_jsp", h.getClassNameForJsp("/blah.jsp"));
assertEquals("org.apache.jsp.blah_jsp", h.getClassNameForJsp("//blah.jsp"));
assertEquals("org.apache.jsp.blah_jsp", h.getClassNameForJsp("blah.jsp"));
assertEquals("org.apache.jsp.a.b.c.blah_jsp", h.getClassNameForJsp("/a/b/c/blah.jsp"));
assertEquals("org.apache.jsp.a.b.c.blah_jsp", h.getClassNameForJsp("a/b/c/blah.jsp"));
}

View File

@ -0,0 +1,225 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.servlet;
import java.io.IOException;
import java.util.EnumSet;
import java.util.EventListener;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Decorator;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class ServletLifeCycleTest
{
static final Queue<String> events = new ConcurrentLinkedQueue<>();
@Test
public void testLifeCycle() throws Exception
{
Server server = new Server(0);
LocalConnector connector = new LocalConnector(server);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler(server, "/");
context.getObjectFactory().addDecorator(new TestDecorator());
ServletHandler sh = context.getServletHandler();
sh.addListener(new ListenerHolder(TestListener.class));
context.addEventListener(context.getServletContext().createListener(TestListener2.class));
sh.addFilterWithMapping(TestFilter.class,"/*", EnumSet.of(DispatcherType.REQUEST));
sh.addFilterWithMapping(new FilterHolder(context.getServletContext().createFilter(TestFilter2.class)),"/*", EnumSet.of(DispatcherType.REQUEST));
sh.addServletWithMapping(TestServlet.class, "/1/*").setInitOrder(1);
sh.addServletWithMapping(TestServlet2.class, "/2/*").setInitOrder(-1);
sh.addServletWithMapping(new ServletHolder(context.getServletContext().createServlet(TestServlet3.class)) {{setInitOrder(1);}}, "/3/*");
assertThat(events, Matchers.contains(
"Decorate class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestListener2",
"Decorate class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestFilter2",
"Decorate class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet3"));
events.clear();
server.start();
assertThat(events, Matchers.contains(
"Decorate class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestListener",
"ContextInitialized class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestListener2",
"ContextInitialized class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestListener",
"Decorate class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestFilter",
"init class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestFilter",
"init class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestFilter2",
"Decorate class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet",
"init class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet",
"init class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet3"));
events.clear();
connector.getResponse("GET /2/info HTTP/1.0\r\n\r\n");
assertThat(events, Matchers.contains(
"Decorate class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet2",
"init class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet2",
"doFilter class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestFilter",
"doFilter class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestFilter2",
"service class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet2"));
events.clear();
server.stop();
assertThat(events, Matchers.contains(
"contextDestroyed class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestListener",
"contextDestroyed class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestListener2",
"destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestFilter2",
"Destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestFilter2",
"destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestFilter",
"Destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestFilter",
"Destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet3",
"destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet3",
"Destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet2",
"destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet2",
"Destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet",
"destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestServlet",
"Destroy class org.eclipse.jetty.servlet.ServletLifeCycleTest$TestListener"));
// Listener added before start is not destroyed
EventListener[] listeners = context.getEventListeners();
assertThat(listeners.length, is(1));
assertThat(listeners[0].getClass(), is(TestListener2.class));
}
public static class TestDecorator implements Decorator
{
@Override
public <T> T decorate(T o)
{
events.add("Decorate " + o.getClass());
return o;
}
@Override
public void destroy(Object o)
{
events.add("Destroy " + o.getClass());
}
}
public static class TestListener implements ServletContextListener
{
@Override
public void contextInitialized(ServletContextEvent sce)
{
events.add("ContextInitialized " + this.getClass());
}
@Override
public void contextDestroyed(ServletContextEvent sce)
{
events.add("contextDestroyed " + this.getClass());
}
}
public static class TestListener2 extends TestListener
{
}
public static class TestFilter implements Filter
{
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
events.add("init " + this.getClass());
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
events.add("doFilter " + this.getClass());
chain.doFilter(request, response);
}
@Override
public void destroy()
{
events.add("destroy " + this.getClass());
}
}
public static class TestFilter2 extends TestFilter
{
}
public static class TestServlet implements Servlet
{
@Override
public void init(ServletConfig config) throws ServletException
{
events.add("init " + this.getClass());
}
@Override
public ServletConfig getServletConfig()
{
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException
{
events.add("service " + this.getClass());
}
@Override
public String getServletInfo()
{
return null;
}
@Override
public void destroy()
{
events.add("destroy " + this.getClass());
}
}
public static class TestServlet2 extends TestServlet
{
}
public static class TestServlet3 extends TestServlet
{
}
}

View File

@ -36,6 +36,7 @@ public class Constraint implements Cloneable, Serializable
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)
{
@ -48,7 +49,8 @@ public class Constraint implements Cloneable, Serializable
method.equals(__CERT_AUTH) ||
method.equals(__CERT_AUTH2) ||
method.equals(__SPNEGO_AUTH) ||
method.equals(__NEGOTIATE_AUTH));
method.equals(__NEGOTIATE_AUTH) ||
method.equals(__OPENID_AUTH));
}
public static final int DC_UNSET = -1;

View File

@ -34,7 +34,6 @@ import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.CloseInfo;
@ -46,14 +45,13 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
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.assertTrue;
public class WebSocketConnectionStatsTest
public class WebSocketStatsTest
{
public static class MyWebSocketServlet extends WebSocketServlet
{
@ -121,7 +119,6 @@ public class WebSocketConnectionStatsTest
return buffer.position() - pos;
}
@Disabled("Flaky test see issue #3982")
@Test
public void echoStatsTest() throws Exception
{
@ -145,12 +142,11 @@ public class WebSocketConnectionStatsTest
{
session.getRemote().sendString(msgText);
}
session.close(StatusCode.NORMAL, null);
assertTrue(socket.closed.await(5, TimeUnit.SECONDS));
assertTrue(wsConnectionClosed.await(5, TimeUnit.SECONDS));
}
assertTrue(socket.closed.await(5, TimeUnit.SECONDS));
assertTrue(wsConnectionClosed.await(5, TimeUnit.SECONDS));
assertThat(statistics.getConnectionsMax(), is(1L));
assertThat(statistics.getConnections(), is(0L));
@ -164,12 +160,6 @@ public class WebSocketConnectionStatsTest
final long closeFrameSize = getFrameByteSize(closeFrame);
final int maskSize = 4; // We use 4 byte mask for client frames
// Pointless Sanity Checks
// assertThat("Upgrade Sent Bytes", upgradeSentBytes, is(197L));
// assertThat("Upgrade Received Bytes", upgradeReceivedBytes, is(261L));
// assertThat("Text Frame Size", textFrameSize, is(13L));
// assertThat("Close Frame Size", closeFrameSize, is(4L));
final long expectedSent = upgradeSentBytes + numMessages * textFrameSize + closeFrameSize;
final long expectedReceived = upgradeReceivedBytes + numMessages * (textFrameSize + maskSize) + closeFrameSize + maskSize;

View File

@ -65,7 +65,6 @@ public class Parser
// Stats (where a message is defined as a WebSocket frame)
private final LongAdder messagesIn = new LongAdder();
private final LongAdder bytesIn = new LongAdder();
// State specific
private State state = State.START;
@ -250,8 +249,6 @@ public class Parser
try
{
int startingBytes = buffer.remaining();
// attempt to parse a frame from the buffer
if (parseFrame(buffer))
{
@ -266,8 +263,6 @@ public class Parser
}
reset();
}
bytesIn.add(startingBytes - buffer.remaining());
}
catch (Throwable t)
{
@ -657,11 +652,6 @@ public class Parser
return messagesIn.longValue();
}
public long getBytesIn()
{
return bytesIn.longValue();
}
@Override
public String toString()
{

View File

@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.AbstractEndPoint;
@ -100,6 +101,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
}
}
@Deprecated
public static class Stats
{
private AtomicLong countFillInterestedEvents = new AtomicLong(0);
@ -139,6 +141,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
private final ConnectionState connectionState = new ConnectionState();
private final FrameFlusher flusher;
private final String id;
private final LongAdder bytesIn = new LongAdder();
private WebSocketSession session;
private List<ExtensionConfig> extensions = new ArrayList<>();
private ByteBuffer prefillBuffer;
@ -400,6 +403,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
return scheduler;
}
@Deprecated()
public Stats getStats()
{
return stats;
@ -472,6 +476,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
return;
}
bytesIn.add(filled);
if (LOG.isDebugEnabled())
LOG.debug("Filled {} bytes - {}", filled, BufferUtil.toDetailString(buffer));
}
@ -668,7 +673,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp
@Override
public long getBytesIn()
{
return parser.getBytesIn();
return bytesIn.longValue();
}
/**

View File

@ -94,6 +94,7 @@
<module>jetty-server</module>
<module>jetty-xml</module>
<module>jetty-security</module>
<module>jetty-openid</module>
<module>jetty-servlet</module>
<module>jetty-webapp</module>
<module>jetty-fcgi</module>
@ -518,7 +519,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-invoker-plugin</artifactId>
<version>3.2.1-SNAPSHOT</version>
<version>3.2.1</version>
<configuration>
<mergeUserSettings>true</mergeUserSettings>
<writeJunitReport>true</writeJunitReport>