mirror of
https://github.com/jetty/jetty.project.git
synced 2025-02-21 06:05:19 +00:00
Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-9.4.x-1036-SchedulerThreads
This commit is contained in:
commit
bcf6b4c581
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
64
jetty-openid/pom.xml
Normal 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>
|
29
jetty-openid/src/main/config/etc/jetty-openid.xml
Normal file
29
jetty-openid/src/main/config/etc/jetty-openid.xml
Normal 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>
|
34
jetty-openid/src/main/config/modules/openid.mod
Normal file
34
jetty-openid/src/main/config/modules/openid.mod
Normal 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
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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" + "\"" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
}
|
3
jetty-openid/src/test/resources/jetty-logging.properties
Executable file
3
jetty-openid/src/test/resources/jetty-logging.properties
Executable 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
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
3
pom.xml
3
pom.xml
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user