Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-10.0.x-1743-refactor-maven-plugin-redux

Signed-off-by: Jan Bartel <janb@webtide.com>
This commit is contained in:
Jan Bartel 2019-11-11 11:28:32 +11:00
commit 8faafdd063
49 changed files with 1794 additions and 472 deletions

View File

@ -23,7 +23,7 @@ Documentation
Project documentation is available on the Jetty Eclipse website.
- [http://www.eclipse.org/jetty/documentation](http://www.eclipse.org/jetty/documentation)
- [https://www.eclipse.org/jetty/documentation](https://www.eclipse.org/jetty/documentation)
Building
========
@ -45,4 +45,4 @@ It is possible to bypass tests by building with `mvn clean install -DskipTests`.
Professional Services
---------------------
Expert advice and production support are available through [Webtide.com](http://webtide.com).
Expert advice and production support are available through [Webtide.com](https://webtide.com).

View File

@ -460,6 +460,11 @@ jetty-9.4.15.v20190215 - 15 February 2019
+ 3350 Do not expect to be able to connect to https URLs with the HttpClient
created from a parameterless constructor
jetty-9.3.28.v20191105 - 05 November 2019
+ 3989 Inform custom ManagedSelector of dead selector via optional
onFailedSelect()
+ 4217 SslConnection.DecryptedEnpoint.flush eternal busy loop
jetty-9.3.27.v20190418 - 18 April 2019
+ 3549 Directory Listing on Windows reveals Resource Base path
+ 3555 DefaultHandler Reveals Base Resource Path of each Context
@ -472,6 +477,9 @@ jetty-9.3.26.v20190403 - 03 April 2019
ForwardedRequestCustomizer
+ 3319 Allow reverse sort for directory listed files
jetty-9.2.29.v20191105 - 05 November 2019
+ 4217 SslConnection.DecryptedEnpoint.flush eternal busy loop
jetty-9.2.28.v20190418 - 18 April 2019
+ 3549 Directory Listing on Windows reveals Resource Base path
+ 3555 DefaultHandler Reveals Base Resource Path of each Context

View File

@ -100,8 +100,8 @@
<GITDOCURL>https://github.com/eclipse/jetty.project/tree/jetty-10.0.x-doc-refactor/jetty-documentation/src/main/asciidoc</GITDOCURL>
<DISTGUIDE>../distribution-guide/index.html</DISTGUIDE>
<EMBEDGUIDE>../embedded-guide/index.html</EMBEDGUIDE>
<QUICKGUIDE>../quickstart-guideindex.html</QUICKGUIDE>
<CONTRIBGUIDE>../contribution-guideindex.html</CONTRIBGUIDE>
<QUICKGUIDE>../quickstart-guide/index.html</QUICKGUIDE>
<CONTRIBGUIDE>../contribution-guide/index.html</CONTRIBGUIDE>
<MVNCENTRAL>http://central.maven.org/maven2</MVNCENTRAL>
<VERSION>${project.version}</VERSION>
<TIMESTAMP>${maven.build.timestamp}</TIMESTAMP>

View File

@ -708,7 +708,7 @@ public class HttpParser
case LF:
setState(State.HEADER);
handle |= _responseHandler.startResponse(_version, _responseStatus, null);
handle = _responseHandler.startResponse(_version, _responseStatus, null);
break;
default:
@ -805,7 +805,7 @@ public class HttpParser
if (_responseHandler != null)
{
setState(State.HEADER);
handle |= _responseHandler.startResponse(_version, _responseStatus, null);
handle = _responseHandler.startResponse(_version, _responseStatus, null);
}
else
{
@ -835,15 +835,13 @@ public class HttpParser
checkVersion();
// Should we try to cache header fields?
if (_fieldCache == null && _version.getVersion() >= HttpVersion.HTTP_1_1.getVersion() && _handler.getHeaderCacheSize() > 0)
{
int headerCache = _handler.getHeaderCacheSize();
int headerCache = _handler.getHeaderCacheSize();
if (_fieldCache == null && _version.getVersion() >= HttpVersion.HTTP_1_1.getVersion() && headerCache > 0)
_fieldCache = new ArrayTernaryTrie<>(headerCache);
}
setState(State.HEADER);
handle |= _requestHandler.startRequest(_methodString, _uri.toString(), _version);
handle = _requestHandler.startRequest(_methodString, _uri.toString(), _version);
continue;
case ALPHA:
@ -865,7 +863,7 @@ public class HttpParser
case LF:
String reason = takeString();
setState(State.HEADER);
handle |= _responseHandler.startResponse(_version, _responseStatus, reason);
handle = _responseHandler.startResponse(_version, _responseStatus, reason);
continue;
case ALPHA:
@ -1589,7 +1587,6 @@ public class HttpParser
}
// Handle _content
byte ch;
while (_state.ordinal() < State.TRAILER.ordinal() && remaining > 0)
{
switch (_state)

View File

@ -23,6 +23,24 @@
<onlyAnalyze>org.eclipse.jetty.jaspi.*</onlyAnalyze>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>manifest</goal>
</goals>
<configuration>
<instructions>
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
<Provide-Capability>osgi.serviceloader;osgi.serviceloader=org.eclipse.jetty.security.Authenticator$Factory</Provide-Capability>
</instructions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -16,6 +16,9 @@
// ========================================================================
//
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.jaspi.JaspiAuthenticatorFactory;
module org.eclipse.jetty.security.jaspi
{
exports org.eclipse.jetty.security.jaspi;
@ -28,4 +31,6 @@ module org.eclipse.jetty.security.jaspi
requires org.eclipse.jetty.security;
requires org.eclipse.jetty.server;
requires org.eclipse.jetty.util;
provides Authenticator.Factory with JaspiAuthenticatorFactory;
}

View File

@ -0,0 +1 @@
org.eclipse.jetty.security.jaspi.JaspiAuthenticatorFactory

View File

@ -24,6 +24,24 @@
<onlyAnalyze>org.eclipse.jetty.security.openid.*</onlyAnalyze>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>manifest</goal>
</goals>
<configuration>
<instructions>
<Require-Capability>osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
<Provide-Capability>osgi.serviceloader;osgi.serviceloader=org.eclipse.jetty.security.Authenticator$Factory</Provide-Capability>
</instructions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -21,13 +21,12 @@ 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
public class OpenIdAuthenticatorFactory implements Authenticator.Factory
{
@Override
public Authenticator getAuthenticator(Server server, ServletContext context, Authenticator.AuthConfiguration configuration, IdentityService identityService, LoginService loginService)
@ -35,6 +34,6 @@ public class OpenIdAuthenticatorFactory extends DefaultAuthenticatorFactory
String auth = configuration.getAuthMethod();
if (Constraint.__OPENID_AUTH.equalsIgnoreCase(auth))
return new OpenIdAuthenticator();
return super.getAuthenticator(server, context, configuration, identityService, loginService);
return null;
}
}

View File

@ -0,0 +1 @@
org.eclipse.jetty.security.openid.OpenIdAuthenticatorFactory

View File

@ -24,6 +24,23 @@
<onlyAnalyze>org.eclipse.jetty.security.*</onlyAnalyze>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>manifest</goal>
</goals>
<configuration>
<instructions>
<Require-Capability>osgi.serviceloader; filter:="(osgi.serviceloader=org.eclipse.jetty.security.Authenticator$Factory)";resolution:=optional;cardinality:=multiple, osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)";resolution:=optional</Require-Capability>
</instructions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -16,6 +16,8 @@
// ========================================================================
//
import org.eclipse.jetty.security.Authenticator;
module org.eclipse.jetty.security
{
exports org.eclipse.jetty.security;
@ -30,4 +32,6 @@ module org.eclipse.jetty.security
requires static java.sql;
// Only required if using SPNEGO.
requires static java.security.jgss;
uses Authenticator.Factory;
}

View File

@ -20,10 +20,13 @@ package org.eclipse.jetty.security;
import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@ -38,6 +41,7 @@ import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandler.Context;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -58,20 +62,31 @@ import org.eclipse.jetty.util.log.Logger;
public abstract class SecurityHandler extends HandlerWrapper implements Authenticator.AuthConfiguration
{
private static final Logger LOG = Log.getLogger(SecurityHandler.class);
private static final List<Authenticator.Factory> __knownAuthenticatorFactories = new ArrayList<>();
private boolean _checkWelcomeFiles = false;
private Authenticator _authenticator;
private Authenticator.Factory _authenticatorFactory = new DefaultAuthenticatorFactory();
private Authenticator.Factory _authenticatorFactory;
private String _realmName;
private String _authMethod;
private final Map<String, String> _initParameters = new HashMap<String, String>();
private final Map<String, String> _initParameters = new HashMap<>();
private LoginService _loginService;
private IdentityService _identityService;
private boolean _renewSession = true;
static
{
for (Authenticator.Factory factory : ServiceLoader.load(Authenticator.Factory.class))
{
__knownAuthenticatorFactories.add(factory);
}
__knownAuthenticatorFactories.add(new DefaultAuthenticatorFactory());
}
protected SecurityHandler()
{
addBean(_authenticatorFactory);
addBean(new DumpableCollection("knownAuthenticatorFactories", __knownAuthenticatorFactories));
}
/**
@ -163,6 +178,14 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti
_authenticatorFactory = authenticatorFactory;
}
/**
* @return the list of discovered authenticatorFactories
*/
public List<Authenticator.Factory> getKnownAuthenticatorFactories()
{
return __knownAuthenticatorFactories;
}
/**
* @return the realmName
*/
@ -241,12 +264,12 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti
* @param key the init key
* @param value the init value
* @return previous value
* @throws IllegalStateException if the SecurityHandler is running
* @throws IllegalStateException if the SecurityHandler is started
*/
public String setInitParameter(String key, String value)
{
if (isRunning())
throw new IllegalStateException("running");
if (isStarted())
throw new IllegalStateException("started");
return _initParameters.put(key, value);
}
@ -336,9 +359,40 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti
throw new IllegalStateException("LoginService has different IdentityService to " + this);
}
Authenticator.Factory authenticatorFactory = getAuthenticatorFactory();
if (_authenticator == null && authenticatorFactory != null && _identityService != null)
setAuthenticator(authenticatorFactory.getAuthenticator(getServer(), ContextHandler.getCurrentContext(), this, _identityService, _loginService));
if (_authenticator == null && _identityService != null)
{
// If someone has set an authenticator factory only use that, otherwise try the list of discovered factories.
if (_authenticatorFactory != null)
{
Authenticator authenticator = _authenticatorFactory.getAuthenticator(getServer(), ContextHandler.getCurrentContext(),
this, _identityService, _loginService);
if (authenticator != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Created authenticator {} with {}", authenticator, _authenticatorFactory);
setAuthenticator(authenticator);
}
}
else
{
for (Authenticator.Factory factory : getKnownAuthenticatorFactories())
{
Authenticator authenticator = factory.getAuthenticator(getServer(), ContextHandler.getCurrentContext(),
this, _identityService, _loginService);
if (authenticator != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Created authenticator {} with {}", authenticator, factory);
setAuthenticator(authenticator);
break;
}
}
}
}
if (_authenticator != null)
_authenticator.setConfiguration(this);

View File

@ -27,11 +27,9 @@ import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@ -106,15 +104,23 @@ public class ErrorHandler extends AbstractHandler
{
if (errorDispatcher != null)
{
errorDispatcher.error(request, response);
}
else
{
String message = (String)request.getAttribute(Dispatcher.ERROR_MESSAGE);
if (message == null)
message = baseRequest.getResponse().getReason();
generateAcceptableResponse(baseRequest, request, response, response.getStatus(), message);
try
{
errorDispatcher.error(request, response);
return;
}
catch (ServletException e)
{
LOG.debug(e);
if (response.isCommitted())
return;
}
}
String message = (String)request.getAttribute(Dispatcher.ERROR_MESSAGE);
if (message == null)
message = baseRequest.getResponse().getReason();
generateAcceptableResponse(baseRequest, request, response, response.getStatus(), message);
}
finally
{

View File

@ -601,11 +601,11 @@ public class Main
}
catch (ConnectException e)
{
usageExit(e, ERR_NOT_STOPPED, jsvcStartArgs.isTestingModeEnabled());
usageExit(e, ERR_NOT_STOPPED, jsvcStartArgs != null && jsvcStartArgs.isTestingModeEnabled());
}
catch (Exception e)
{
usageExit(e, ERR_UNKNOWN, jsvcStartArgs.isTestingModeEnabled());
usageExit(e, ERR_UNKNOWN, jsvcStartArgs != null && jsvcStartArgs.isTestingModeEnabled());
}
}

View File

@ -381,4 +381,11 @@ public class JarFileResource extends JarResource
URL url = new URL(string);
return url.sameFile(resource.getURI().toURL());
}
public File getJarFile()
{
if (_file != null)
return _file;
return null;
}
}

View File

@ -20,15 +20,17 @@ package org.eclipse.jetty.webapp;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.Locale;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.JarFileResource;
import org.eclipse.jetty.util.resource.JarResource;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
@ -497,7 +499,7 @@ public class WebInfConfiguration extends AbstractConfiguration
}
}
//Resource base
// Resource base
try
{
Resource resource = context.getBaseResource();
@ -506,27 +508,21 @@ public class WebInfConfiguration extends AbstractConfiguration
if (context.getWar() == null || context.getWar().length() == 0)
throw new IllegalStateException("No resourceBase or war set for context");
// Set dir or WAR
// Set dir or WAR to resource
resource = context.newResource(context.getWar());
}
if (resource.getURI().getPath() != null)
{
String tmp = URIUtil.decodePath(resource.getURI().getPath());
if (tmp.endsWith("/"))
tmp = tmp.substring(0, tmp.length() - 1);
if (tmp.endsWith("!"))
tmp = tmp.substring(0, tmp.length() - 1);
//get just the last part which is the filename
int i = tmp.lastIndexOf("/");
canonicalName.append(tmp.substring(i + 1, tmp.length()));
}
String resourceBaseName = getResourceBaseName(resource);
canonicalName.append(resourceBaseName);
canonicalName.append("-");
}
catch (Exception e)
{
LOG.warn("Can't generate resourceBase as part of webapp tmp dir name " + e);
LOG.debug(e);
if (LOG.isDebugEnabled())
{
LOG.debug("Can't get resource base name", e);
}
canonicalName.append("-"); // empty resourceBaseName segment
}
//Context name
@ -553,6 +549,70 @@ public class WebInfConfiguration extends AbstractConfiguration
canonicalName.append("-");
return canonicalName.toString();
return StringUtil.sanitizeFileSystemName(canonicalName.toString());
}
protected static String getResourceBaseName(Resource resource)
{
// Use File System and File interface if present
try
{
File resourceFile = resource.getFile();
if ((resourceFile != null) && (resource instanceof JarFileResource))
{
resourceFile = ((JarFileResource)resource).getJarFile();
}
if (resourceFile != null)
{
return resourceFile.getName();
}
}
catch (IOException e)
{
if (LOG.isDebugEnabled())
{
LOG.debug("Resource has no File reference: {}", resource);
}
}
// Use URI itself.
URI uri = resource.getURI();
if (uri == null)
{
if (LOG.isDebugEnabled())
{
LOG.debug("Resource has no URI reference: {}", resource);
}
return "";
}
return getUriLastPathSegment(uri);
}
protected static String getUriLastPathSegment(URI uri)
{
String ssp = uri.getSchemeSpecificPart();
// strip off deep jar:file: reference information
int idx = ssp.indexOf("!/");
if (idx != -1)
{
ssp = ssp.substring(0, idx);
}
// Strip off trailing '/' if present
if (ssp.endsWith("/"))
{
ssp = ssp.substring(0, ssp.length() - 1);
}
// Only interested in last segment
idx = ssp.lastIndexOf('/');
if (idx != -1)
{
ssp = ssp.substring(idx + 1);
}
return ssp;
}
}

View File

@ -0,0 +1,211 @@
//
// ========================================================================
// 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.webapp;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.PathResource;
import org.eclipse.jetty.util.resource.Resource;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
/**
* WebInfConfigurationTest
*/
@ExtendWith(WorkDirExtension.class)
public class WebInfConfigurationTest
{
private static final Logger LOG = Log.getLogger(WebInfConfigurationTest.class);
private static final String TEST_RESOURCE_JAR = "test-base-resource.jar";
public WorkDir workDir;
public static Stream<Arguments> rawResourceNames()
{
return Stream.of(
Arguments.of("/", ""),
Arguments.of("/a", "a")
);
}
@ParameterizedTest
@MethodSource("rawResourceNames")
public void testTinyGetResourceBaseName(String rawPath, String expectedName) throws IOException
{
Resource resource = Resource.newResource(rawPath);
assertThat(WebInfConfiguration.getResourceBaseName(resource), is(expectedName));
}
public static Stream<Arguments> fileBaseResourceNames()
{
return Stream.of(
Arguments.of("test.war", "test.war"),
Arguments.of("a/b/c/test.war", "test.war"),
Arguments.of("bar%2Fbaz/test.war", "test.war"),
Arguments.of("fizz buzz/test.war", "test.war"),
Arguments.of("another one/bites the dust/", "bites the dust"),
Arguments.of("another+one/bites+the+dust/", "bites+the+dust"),
Arguments.of("another%20one/bites%20the%20dust/", "bites%20the%20dust"),
Arguments.of("spanish/n\u00FAmero.war", "n\u00FAmero.war"),
Arguments.of("spanish/n%C3%BAmero.war", "n%C3%BAmero.war"),
Arguments.of("a/b!/", "b!"),
Arguments.of("a/b!/c/", "c"),
Arguments.of("a/b!/c/d/", "d"),
Arguments.of("a/b%21/", "b%21")
);
}
@ParameterizedTest
@MethodSource("fileBaseResourceNames")
public void testPathGetResourceBaseName(String basePath, String expectedName) throws IOException
{
Path root = workDir.getPath();
Path base = root.resolve(basePath);
if (basePath.endsWith("/"))
{
// we are working with a directory.
FS.ensureDirExists(base);
}
else
{
FS.ensureDirExists(base.getParent());
FS.touch(base);
}
Resource resource = new PathResource(base);
assertThat(WebInfConfiguration.getResourceBaseName(resource), is(expectedName));
}
public static Stream<Arguments> fileUriBaseResourceNames()
{
return Stream.of(
Arguments.of("test.war", "test.war"),
Arguments.of("a/b/c/test.war", "test.war"),
Arguments.of("bar%2Fbaz/test.war", "test.war"),
Arguments.of("fizz buzz/test.war", "test.war"),
Arguments.of("another one/bites the dust/", "bites the dust"),
Arguments.of("another+one/bites+the+dust/", "bites+the+dust"),
Arguments.of("another%20one/bites%20the%20dust/", "bites%20the%20dust"),
Arguments.of("spanish/n\u00FAmero.war", "n\u00FAmero.war"),
Arguments.of("spanish/n%C3%BAmero.war", "n%C3%BAmero.war"),
Arguments.of("a/b!/", "b"),
Arguments.of("a/b!/c/", "b"),
Arguments.of("a/b!/c/d/", "b"),
Arguments.of("a/b%21/", "b%21")
);
}
/**
* Similar to testPathGetResourceBaseName, but with "file:" URIs
*/
@ParameterizedTest
@MethodSource("fileUriBaseResourceNames")
public void testFileUriGetUriLastPathSegment(String basePath, String expectedName) throws IOException
{
Path root = workDir.getPath();
Path base = root.resolve(basePath);
if (basePath.endsWith("/"))
{
FS.ensureDirExists(base);
}
else
{
FS.ensureDirExists(base.getParent());
FS.touch(base);
}
URI uri = base.toUri();
if (OS.MAC.isCurrentOs())
{
// Normalize Unicode to NFD form that OSX Path/FileSystem produces
expectedName = Normalizer.normalize(expectedName, Normalizer.Form.NFD);
}
assertThat(WebInfConfiguration.getUriLastPathSegment(uri), is(expectedName));
}
public static Stream<Arguments> uriLastSegmentSource() throws URISyntaxException, IOException
{
Path testJar = MavenTestingUtils.getTestResourcePathFile(TEST_RESOURCE_JAR);
URI uri = new URI("jar", testJar.toUri().toASCIIString(), null);
Map<String, Object> env = new HashMap<>();
env.put("multi-release", "runtime");
List<Arguments> arguments = new ArrayList<>();
arguments.add(Arguments.of(uri, TEST_RESOURCE_JAR));
try (FileSystem zipFs = FileSystems.newFileSystem(uri, env))
{
FileVisitOption[] fileVisitOptions = new FileVisitOption[]{};
for (Path root : zipFs.getRootDirectories())
{
Stream<Path> entryStream = Files.find(root, 10, (path, attrs) -> true, fileVisitOptions);
entryStream.forEach((path) ->
{
if (path.toString().endsWith("!/"))
{
// skip - JAR entry type not supported by Jetty
// TODO: re-enable once we start to use zipfs
LOG.warn("Skipping Unsupported entry: " + path.toUri());
}
else
{
arguments.add(Arguments.of(path.toUri(), TEST_RESOURCE_JAR));
}
});
}
}
return arguments.stream();
}
/**
* Tests of URIs last segment, including "jar:file:" based URIs.
*/
@ParameterizedTest
@MethodSource("uriLastSegmentSource")
public void testGetUriLastPathSegment(URI uri, String expectedName) throws IOException
{
assertThat(WebInfConfiguration.getUriLastPathSegment(uri), is(expectedName));
}
}

Binary file not shown.

View File

@ -545,13 +545,7 @@ public class JavaxWebSocketFrameHandler implements FrameHandler
public void onPing(Frame frame, Callback callback)
{
ByteBuffer payload = BufferUtil.EMPTY_BUFFER;
if (frame.hasPayload())
{
payload = ByteBuffer.allocate(frame.getPayloadLength());
BufferUtil.put(frame.getPayload(), payload);
}
ByteBuffer payload = BufferUtil.copy(frame.getPayload());
coreSession.sendFrame(new Frame(OpCode.PONG).setPayload(payload), Callback.NOOP, false);
callback.succeeded();
}

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.websocket.javax.server.config;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
import javax.websocket.Extension;
@ -28,6 +29,7 @@ import javax.websocket.server.ServerEndpointConfig.Configurator;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.core.ExtensionConfig;
/**
* The "Container Default Configurator" per the JSR-356 spec.
@ -78,7 +80,16 @@ public final class ContainerDefaultConfigurator extends Configurator
@Override
public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested)
{
return requested;
List<Extension> negotiatedExtensions = new ArrayList<>();
for (Extension ext : requested)
{
// Only choose the first extension if multiple with the same name.
long matches = negotiatedExtensions.stream().filter(e -> e.getName().equals(ext.getName())).count();
if (matches == 0)
negotiatedExtensions.add(ext);
}
return negotiatedExtensions;
}
@Override

View File

@ -0,0 +1,18 @@
{
"options": {
"failByDrop": false
},
"outdir": "./target/reports/servers",
"servers": [
{
"agent": "Jetty-10.0.0-SNAPSHOT",
"url": "ws://127.0.0.1:9001",
"options": {
"version": 18
}
}
],
"cases": ["*"],
"exclude-cases": [],
"exclude-agent-cases": {}
}

View File

@ -0,0 +1,10 @@
{
"options": {
"failByDrop": false
},
"url": "ws://127.0.0.1:9001",
"outdir": "./target/reports/clients",
"cases": ["*"],
"exclude-cases": [],
"exclude-agent-cases": {}
}

View File

@ -52,28 +52,32 @@ public class EventSocket
public void onOpen(Session session)
{
this.session = session;
LOG.info("{} onOpen(): {}", toString(), session);
if (LOG.isDebugEnabled())
LOG.debug("{} onOpen(): {}", toString(), session);
openLatch.countDown();
}
@OnMessage
public void onMessage(String message) throws IOException
{
LOG.info("{} onMessage(): {}", toString(), message);
if (LOG.isDebugEnabled())
LOG.debug("{} onMessage(): {}", toString(), message);
messageQueue.offer(message);
}
@OnClose
public void onClose(CloseReason reason)
{
LOG.info("{} onClose(): {}", toString(), reason);
if (LOG.isDebugEnabled())
LOG.debug("{} onClose(): {}", toString(), reason);
closeLatch.countDown();
}
@OnError
public void onError(Throwable cause)
{
LOG.info("{} onError(): {}", toString(), cause);
if (LOG.isDebugEnabled())
LOG.debug("{} onError(): {}", toString(), cause);
error = cause;
}
}

View File

@ -0,0 +1,200 @@
//
// ========================================================================
// 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.websocket.javax.tests.autobahn;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import javax.websocket.CloseReason;
import org.eclipse.jetty.util.Jetty;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.javax.client.JavaxWebSocketClientContainer;
import org.eclipse.jetty.websocket.javax.tests.EventSocket;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* WebSocket Client for use with <a href="https://github.com/crossbario/autobahn-testsuite">autobahn websocket testsuite</a> (wstest).
* <p>
* Installing Autobahn:
* </p>
* <pre>
* # For Debian / Ubuntu
* $ sudo apt-get install python python-dev python-twisted
* $ sudo apt-get install python-pip
* $ sudo pip install autobahntestsuite
*
* # For Fedora / Redhat
* $ sudo yum install python python-dev python-pip twisted
* $ sudo yum install libffi-devel
* $ sudo pip install autobahntestsuite
* </pre>
* <p>
* Upgrading an existing installation of autobahntestsuite
* </p>
* <pre>
* $ sudo pip install -U autobahntestsuite
* </pre>
* <p>
* Running Autobahn Fuzzing Server (which you run this client implementation against):
* </p>
* <pre>
* # Change to javax-websocket-tests directory first.
* $ cd jetty-websocket/javax-websocket-tests/
* $ wstest --mode=fuzzingserver --spec=fuzzingserver.json
*
* # Report output is configured (in the fuzzingserver.json) at location:
* $ ls target/reports/clients/
* </pre>
*/
public class JavaxAutobahnClient
{
public static void main(String[] args)
{
String hostname = "localhost";
int port = 9001;
if (args.length > 0)
hostname = args[0];
if (args.length > 1)
port = Integer.parseInt(args[1]);
// Optional case numbers
// NOTE: these are url query parameter case numbers (whole integers, eg "6"), not the case ids (eg "7.3.1")
int[] caseNumbers = null;
if (args.length > 2)
{
int offset = 2;
caseNumbers = new int[args.length - offset];
for (int i = offset; i < args.length; i++)
{
caseNumbers[i - offset] = Integer.parseInt(args[i]);
}
}
JavaxAutobahnClient client = null;
try
{
String userAgent = "JettyWebsocketClient/" + Jetty.VERSION;
client = new JavaxAutobahnClient(hostname, port, userAgent);
LOG.info("Running test suite...");
LOG.info("Using Fuzzing Server: {}:{}", hostname, port);
LOG.info("User Agent: {}", userAgent);
if (caseNumbers == null)
{
int caseCount = client.getCaseCount();
LOG.info("Will run all {} cases ...", caseCount);
for (int caseNum = 1; caseNum <= caseCount; caseNum++)
{
LOG.info("Running case {} (of {}) ...", caseNum, caseCount);
client.runCaseByNumber(caseNum);
}
}
else
{
LOG.info("Will run %d cases ...", caseNumbers.length);
for (int caseNum : caseNumbers)
{
client.runCaseByNumber(caseNum);
}
}
LOG.info("All test cases executed.");
client.updateReports();
}
catch (Throwable t)
{
LOG.warn("Test Failed", t);
}
finally
{
if (client != null)
client.stop();
}
}
private static final Logger LOG = Log.getLogger(JavaxAutobahnClient.class);
private URI baseWebsocketUri;
private JavaxWebSocketClientContainer clientContainer;
private String userAgent;
public JavaxAutobahnClient(String hostname, int port, String userAgent) throws Exception
{
this.userAgent = userAgent;
this.baseWebsocketUri = new URI("ws://" + hostname + ":" + port);
this.clientContainer = new JavaxWebSocketClientContainer();
clientContainer.start();
}
public void stop()
{
LifeCycle.stop(clientContainer);
}
public int getCaseCount()
{
URI wsUri = baseWebsocketUri.resolve("/getCaseCount");
EventSocket onCaseCount = new EventSocket();
try
{
clientContainer.connectToServer(onCaseCount, wsUri);
String msg = onCaseCount.messageQueue.poll(10, TimeUnit.SECONDS);
onCaseCount.session.close(new CloseReason(CloseReason.CloseCodes.GOING_AWAY, null));
assertTrue(onCaseCount.closeLatch.await(2, TimeUnit.SECONDS));
assertNotNull(msg);
return Integer.decode(msg);
}
catch (Throwable t)
{
throw new IllegalStateException("Unable to get Case Count", t);
}
}
public void runCaseByNumber(int caseNumber) throws Exception
{
URI wsUri = baseWebsocketUri.resolve("/runCase?case=" + caseNumber + "&agent=" + UrlEncoded.encodeString(userAgent));
LOG.info("test uri: {}", wsUri);
JavaxAutobahnSocket echoHandler = new JavaxAutobahnSocket();
clientContainer.connectToServer(echoHandler, wsUri);
// Wait up to 5 min as some of the tests can take a while
if (!echoHandler.closeLatch.await(5, TimeUnit.MINUTES))
{
LOG.warn("could not close {}, closing session", echoHandler);
echoHandler.session.close(new CloseReason(CloseReason.CloseCodes.GOING_AWAY, null));
}
}
public void updateReports() throws Exception
{
URI wsUri = baseWebsocketUri.resolve("/updateReports?agent=" + UrlEncoded.encodeString(userAgent));
EventSocket onUpdateReports = new EventSocket();
clientContainer.connectToServer(onUpdateReports, wsUri);
assertTrue(onUpdateReports.closeLatch.await(15, TimeUnit.SECONDS));
LOG.info("Reports updated.");
LOG.info("Test suite finished!");
}
}

View File

@ -0,0 +1,82 @@
//
// ========================================================================
// 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.websocket.javax.tests.autobahn;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer;
/**
* WebSocket Server for use with <a href="https://github.com/crossbario/autobahn-testsuite">autobahn websocket testsuite</a> (wstest).
* <p>
* Installing Autobahn:
* </p>
* <pre>
* # For Debian / Ubuntu
* $ sudo apt-get install python python-dev python-twisted
* $ sudo apt-get install python-pip
* $ sudo pip install autobahntestsuite
*
* # For Fedora / Redhat
* $ sudo yum install python python-dev python-pip twisted
* $ sudo yum install libffi-devel
* $ sudo pip install autobahntestsuite
* </pre>
* <p>
* Upgrading an existing installation of autobahntestsuite
* </p>
* <pre>
* $ sudo pip install -U autobahntestsuite
* </pre>
* <p>
* Running Autobahn Fuzzing Client (against this server implementation):
* </p>
* <pre>
* # Change to javax-websocket-tests directory first.
* $ cd jetty-websocket/javax-websocket-tests/
* $ wstest --mode=fuzzingclient --spec=fuzzingclient.json
*
* # Report output is configured (in the fuzzingclient.json) at location:
* $ ls target/reports/servers/
* </pre>
*/
public class JavaxAutobahnServer
{
public static void main(String[] args) throws Exception
{
int port = 9001; // same port as found in fuzzing-client.json
if (args != null && args.length > 0)
port = Integer.parseInt(args[0]);
Server server = new Server(port);
ServerConnector connector = new ServerConnector(server);
connector.setIdleTimeout(10000);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
server.setHandler(context);
JavaxWebSocketServletContainerInitializer.configure(context, (servletContext, container)->
container.addEndpoint(JavaxAutobahnSocket.class));
server.start();
server.join();
}
}

View File

@ -0,0 +1,76 @@
//
// ========================================================================
// 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.websocket.javax.tests.autobahn;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import javax.websocket.ClientEndpoint;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ClientEndpoint
@ServerEndpoint("/")
public class JavaxAutobahnSocket
{
private static final Logger LOG = Log.getLogger(JavaxAutobahnSocket.class);
public Session session;
public CountDownLatch closeLatch = new CountDownLatch(1);
@OnOpen
public void onConnect(Session session)
{
this.session = session;
session.setMaxTextMessageBufferSize(Integer.MAX_VALUE);
session.setMaxBinaryMessageBufferSize(Integer.MAX_VALUE);
}
@OnMessage
public void onText(String message) throws IOException
{
session.getBasicRemote().sendText(message);
}
@OnMessage
public void onBinary(ByteBuffer message) throws IOException
{
session.getBasicRemote().sendBinary(message);
}
@OnError
public void onError(Throwable t)
{
if (LOG.isDebugEnabled())
LOG.debug("onError()", t);
}
@OnClose
public void onClose()
{
closeLatch.countDown();
}
}

View File

@ -84,6 +84,20 @@ public interface WebSocketPolicy
*/
long getMaxTextMessageSize();
/**
* The maximum payload size of any WebSocket Frame which can be received.
*
* @return the maximum size of a WebSocket Frame.
*/
long getMaxFrameSize();
/**
* If true, frames are automatically fragmented to respect the maximum frame size.
*
* @return whether to automatically fragment incoming WebSocket Frames.
*/
boolean isAutoFragment();
/**
* The duration that a websocket may be idle before being closed by the implementation
*
@ -123,4 +137,21 @@ public interface WebSocketPolicy
* @param size the maximum allowed size of a text message.
*/
void setMaxTextMessageSize(long size);
/**
* The maximum payload size of any WebSocket Frame which can be received.
* <p>
* WebSocket Frames over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
* </p>
*
* @param maxFrameSize the maximum allowed size of a WebSocket Frame.
*/
void setMaxFrameSize(long maxFrameSize);
/**
* If set to true, frames are automatically fragmented to respect the maximum frame size.
*
* @param autoFragment whether to automatically fragment incoming WebSocket Frames.
*/
void setAutoFragment(boolean autoFragment);
}

View File

@ -45,6 +45,8 @@ import org.eclipse.jetty.websocket.api.Session;
* <p>
* <u>Binary Message Versions</u>
* <ol>
* <li>{@code public void methodName(ByteBuffer message)}</li>
* <li><code>public void methodName({@link Session} session, ByteBuffer message)</code></li>
* <li>{@code public void methodName(byte buf[], int offset, int length)}</li>
* <li><code>public void methodName({@link Session} session, byte buf[], int offset, int length)</code></li>
* <li>{@code public void methodName(InputStream stream)}</li>

View File

@ -41,6 +41,7 @@ import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ShutdownThread;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
@ -65,7 +66,8 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli
private final List<WebSocketSessionListener> sessionListeners = new CopyOnWriteArrayList<>();
private final SessionTracker sessionTracker = new SessionTracker();
private final FrameHandler.ConfigurationCustomizer configurationCustomizer = new FrameHandler.ConfigurationCustomizer();
private WebSocketComponents components = new WebSocketComponents();
private final WebSocketComponents components = new WebSocketComponents();
private boolean stopAtShutdown = false;
/**
* Instantiate a WebSocketClient with defaults
@ -235,6 +237,18 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli
return configurationCustomizer.getMaxTextMessageSize();
}
@Override
public long getMaxFrameSize()
{
return configurationCustomizer.getMaxFrameSize();
}
@Override
public boolean isAutoFragment()
{
return configurationCustomizer.isAutoFragment();
}
@Override
public void setIdleTimeout(Duration duration)
{
@ -266,6 +280,18 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli
configurationCustomizer.setMaxTextMessageSize(size);
}
@Override
public void setMaxFrameSize(long maxFrameSize)
{
configurationCustomizer.setMaxFrameSize(maxFrameSize);
}
@Override
public void setAutoFragment(boolean autoFragment)
{
configurationCustomizer.setAutoFragment(autoFragment);
}
public SocketAddress getBindAddress()
{
return getHttpClient().getBindAddress();
@ -340,6 +366,31 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli
return getHttpClient().getSslContextFactory();
}
/**
* Set JVM shutdown behavior.
* @param stop If true, this client instance will be explicitly stopped when the
* JVM is shutdown. Otherwise the application is responsible for maintaining the WebSocketClient lifecycle.
* @see Runtime#addShutdownHook(Thread)
* @see ShutdownThread
*/
public synchronized void setStopAtShutdown(boolean stop)
{
if (stop)
{
if (!stopAtShutdown && !ShutdownThread.isRegistered(this))
ShutdownThread.register(this);
}
else
ShutdownThread.deregister(this);
stopAtShutdown = stop;
}
public boolean isStopAtShutdown()
{
return stopAtShutdown;
}
@Override
public String toString()
{

View File

@ -29,6 +29,7 @@ import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.common.invoke.InvalidSignatureException;
import org.eclipse.jetty.websocket.core.BadPayloadException;
import org.eclipse.jetty.websocket.core.CloseException;
@ -359,10 +360,8 @@ public class JettyWebSocketFrameHandler implements FrameHandler
else
{
// Automatically respond
Frame pong = new Frame(OpCode.PONG);
if (frame.hasPayload())
pong.setPayload(frame.getPayload());
getSession().getRemote().getCoreSession().sendFrame(pong, Callback.NOOP, false);
ByteBuffer payload = BufferUtil.copy(frame.getPayload());
getSession().getRemote().sendPong(payload, WriteCallback.NOOP);
}
callback.succeeded();
}

View File

@ -117,6 +117,18 @@ public class WebSocketSession extends AbstractLifeCycle implements Session, Susp
return coreSession.getMaxTextMessageSize();
}
@Override
public long getMaxFrameSize()
{
return coreSession.getMaxFrameSize();
}
@Override
public boolean isAutoFragment()
{
return coreSession.isAutoFragment();
}
@Override
public void setIdleTimeout(Duration duration)
{
@ -147,6 +159,18 @@ public class WebSocketSession extends AbstractLifeCycle implements Session, Susp
coreSession.setMaxTextMessageSize(size);
}
@Override
public void setMaxFrameSize(long maxFrameSize)
{
coreSession.setMaxFrameSize(maxFrameSize);
}
@Override
public void setAutoFragment(boolean autoFragment)
{
coreSession.setAutoFragment(autoFragment);
}
@Override
public String getProtocolVersion()
{

View File

@ -295,6 +295,18 @@ public class OutgoingMessageCapture extends FrameHandler.CoreSession.Empty imple
return maxMessageSize;
}
@Override
public long getMaxFrameSize()
{
return 0;
}
@Override
public boolean isAutoFragment()
{
return false;
}
@Override
public void setIdleTimeout(Duration duration)
{
@ -319,6 +331,16 @@ public class OutgoingMessageCapture extends FrameHandler.CoreSession.Empty imple
public void setMaxTextMessageSize(long size)
{
}
@Override
public void setMaxFrameSize(long maxFrameSize)
{
}
@Override
public void setAutoFragment(boolean autoFragment)
{
}
};
}
}

View File

@ -206,6 +206,18 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
return customizer.getMaxTextMessageSize();
}
@Override
public long getMaxFrameSize()
{
return customizer.getMaxFrameSize();
}
@Override
public boolean isAutoFragment()
{
return customizer.isAutoFragment();
}
@Override
public void setIdleTimeout(Duration duration)
{
@ -235,4 +247,16 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements
{
customizer.setMaxTextMessageSize(size);
}
@Override
public void setMaxFrameSize(long maxFrameSize)
{
customizer.setMaxFrameSize(maxFrameSize);
}
@Override
public void setAutoFragment(boolean autoFragment)
{
customizer.setAutoFragment(autoFragment);
}
}

View File

@ -22,7 +22,6 @@ import java.time.Duration;
import java.util.Set;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
@ -50,44 +49,25 @@ public class JettyWebSocketServletFactory implements WebSocketPolicy
return WebSocketBehavior.SERVER;
}
/**
* If true, frames are automatically fragmented to respect the maximum frame size.
*
* @return whether to automatically fragment incoming WebSocket Frames.
*/
@Override
public boolean isAutoFragment()
{
return factory.isAutoFragment();
}
/**
* If set to true, frames are automatically fragmented to respect the maximum frame size.
*
* @param autoFragment whether to automatically fragment incoming WebSocket Frames.
*/
@Override
public void setAutoFragment(boolean autoFragment)
{
factory.setAutoFragment(autoFragment);
}
/**
* The maximum payload size of any WebSocket Frame which can be received.
*
* @return the maximum size of a WebSocket Frame.
*/
@Override
public long getMaxFrameSize()
{
return factory.getMaxFrameSize();
}
/**
* The maximum payload size of any WebSocket Frame which can be received.
* <p>
* WebSocket Frames over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
* </p>
*
* @param maxFrameSize the maximum allowed size of a WebSocket Frame.
*/
@Override
public void setMaxFrameSize(long maxFrameSize)
{
factory.setMaxFrameSize(maxFrameSize);

View File

@ -0,0 +1,18 @@
{
"options": {
"failByDrop": false
},
"outdir": "./target/reports/servers",
"servers": [
{
"agent": "Jetty-10.0.0-SNAPSHOT",
"url": "ws://127.0.0.1:9001",
"options": {
"version": 18
}
}
],
"cases": ["*"],
"exclude-cases": [],
"exclude-agent-cases": {}
}

View File

@ -0,0 +1,10 @@
{
"options": {
"failByDrop": false
},
"url": "ws://127.0.0.1:9001",
"outdir": "./target/reports/clients",
"cases": ["*"],
"exclude-cases": [],
"exclude-agent-cases": {}
}

View File

@ -57,14 +57,16 @@ public class EventSocket
{
this.session = session;
behavior = session.getPolicy().getBehavior().name();
LOG.info("{} onOpen(): {}", toString(), session);
if (LOG.isDebugEnabled())
LOG.debug("{} onOpen(): {}", toString(), session);
openLatch.countDown();
}
@OnWebSocketMessage
public void onMessage(String message) throws IOException
{
LOG.info("{} onMessage(): {}", toString(), message);
if (LOG.isDebugEnabled())
LOG.debug("{} onMessage(): {}", toString(), message);
messageQueue.offer(message);
}
@ -72,14 +74,16 @@ public class EventSocket
public void onMessage(byte buf[], int offset, int len)
{
ByteBuffer message = ByteBuffer.wrap(buf, offset, len);
LOG.info("{} onMessage(): {}", toString(), message);
if (LOG.isDebugEnabled())
LOG.debug("{} onMessage(): {}", toString(), message);
binaryMessageQueue.offer(message);
}
@OnWebSocketClose
public void onClose(int statusCode, String reason)
{
LOG.info("{} onClose(): {}:{}", toString(), statusCode, reason);
if (LOG.isDebugEnabled())
LOG.debug("{} onClose(): {}:{}", toString(), statusCode, reason);
this.statusCode = statusCode;
this.reason = reason;
closeLatch.countDown();
@ -88,7 +92,8 @@ public class EventSocket
@OnWebSocketError
public void onError(Throwable cause)
{
LOG.info("{} onError(): {}", toString(), cause);
if (LOG.isDebugEnabled())
LOG.debug("{} onError(): {}", toString(), cause);
error = cause;
errorLatch.countDown();
}

View File

@ -0,0 +1,227 @@
//
// ========================================================================
// 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.websocket.tests.autobahn;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.util.Jetty;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.tests.EchoSocket;
import org.eclipse.jetty.websocket.tests.EventSocket;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* WebSocket Client for use with <a href="https://github.com/crossbario/autobahn-testsuite">autobahn websocket testsuite</a> (wstest).
* <p>
* Installing Autobahn:
* </p>
* <pre>
* # For Debian / Ubuntu
* $ sudo apt-get install python python-dev python-twisted
* $ sudo apt-get install python-pip
* $ sudo pip install autobahntestsuite
*
* # For Fedora / Redhat
* $ sudo yum install python python-dev python-pip twisted
* $ sudo yum install libffi-devel
* $ sudo pip install autobahntestsuite
* </pre>
* <p>
* Upgrading an existing installation of autobahntestsuite
* </p>
* <pre>
* $ sudo pip install -U autobahntestsuite
* </pre>
* <p>
* Running Autobahn Fuzzing Server (which you run this client implementation against):
* </p>
* <pre>
* # Change to jetty-websocket-tests directory first.
* $ cd jetty-websocket/jetty-websocket-tests/
* $ wstest --mode=fuzzingserver --spec=fuzzingserver.json
*
* # Report output is configured (in the fuzzingserver.json) at location:
* $ ls target/reports/clients/
* </pre>
*/
public class JettyAutobahnClient
{
public static void main(String[] args)
{
String hostname = "localhost";
int port = 9001;
if (args.length > 0)
hostname = args[0];
if (args.length > 1)
port = Integer.parseInt(args[1]);
// Optional case numbers
// NOTE: these are url query parameter case numbers (whole integers, eg "6"), not the case ids (eg "7.3.1")
int[] caseNumbers = null;
if (args.length > 2)
{
int offset = 2;
caseNumbers = new int[args.length - offset];
for (int i = offset; i < args.length; i++)
{
caseNumbers[i - offset] = Integer.parseInt(args[i]);
}
}
JettyAutobahnClient client = null;
try
{
String userAgent = "JettyWebsocketClient/" + Jetty.VERSION;
client = new JettyAutobahnClient(hostname, port, userAgent);
LOG.info("Running test suite...");
LOG.info("Using Fuzzing Server: {}:{}", hostname, port);
LOG.info("User Agent: {}", userAgent);
if (caseNumbers == null)
{
int caseCount = client.getCaseCount();
LOG.info("Will run all {} cases ...", caseCount);
for (int caseNum = 1; caseNum <= caseCount; caseNum++)
{
LOG.info("Running case {} (of {}) ...", caseNum, caseCount);
client.runCaseByNumber(caseNum);
}
}
else
{
LOG.info("Will run %d cases ...", caseNumbers.length);
for (int caseNum : caseNumbers)
{
client.runCaseByNumber(caseNum);
}
}
LOG.info("All test cases executed.");
client.updateReports();
}
catch (Throwable t)
{
LOG.warn("Test Failed", t);
}
finally
{
if (client != null)
client.shutdown();
}
}
private static final Logger LOG = Log.getLogger(JettyAutobahnClient.class);
private URI baseWebsocketUri;
private WebSocketClient client;
private String userAgent;
public JettyAutobahnClient(String hostname, int port, String userAgent) throws Exception
{
this.userAgent = userAgent;
this.baseWebsocketUri = new URI("ws://" + hostname + ":" + port);
this.client = new WebSocketClient();
this.client.start();
}
public int getCaseCount() throws IOException, InterruptedException
{
URI wsUri = baseWebsocketUri.resolve("/getCaseCount");
EventSocket onCaseCount = new EventSocket();
CompletableFuture<Session> response = client.connect(onCaseCount, wsUri);
if (waitForUpgrade(wsUri, response))
{
String msg = onCaseCount.messageQueue.poll(10, TimeUnit.SECONDS);
onCaseCount.session.close(StatusCode.SHUTDOWN, null);
assertTrue(onCaseCount.closeLatch.await(2, TimeUnit.SECONDS));
assertNotNull(msg);
return Integer.decode(msg);
}
throw new IllegalStateException("Unable to get Case Count");
}
public void runCaseByNumber(int caseNumber) throws IOException, InterruptedException
{
URI wsUri = baseWebsocketUri.resolve("/runCase?case=" + caseNumber + "&agent=" + UrlEncoded.encodeString(userAgent));
LOG.info("test uri: {}", wsUri);
EchoSocket echoHandler = new JettyAutobahnSocket();
Future<Session> response = client.connect(echoHandler, wsUri);
if (waitForUpgrade(wsUri, response))
{
// Wait up to 5 min as some of the tests can take a while
if (!echoHandler.closeLatch.await(5, TimeUnit.MINUTES))
{
LOG.warn("could not close {}, aborting session", echoHandler);
echoHandler.session.disconnect();
}
}
}
public void shutdown()
{
try
{
client.stop();
}
catch (Exception e)
{
LOG.warn("Unable to stop WebSocketClient", e);
}
}
public void updateReports() throws IOException, InterruptedException, ExecutionException, TimeoutException
{
URI wsUri = baseWebsocketUri.resolve("/updateReports?agent=" + UrlEncoded.encodeString(userAgent));
EventSocket onUpdateReports = new EventSocket();
Future<Session> response = client.connect(onUpdateReports, wsUri);
response.get(5, TimeUnit.SECONDS);
assertTrue(onUpdateReports.closeLatch.await(15, TimeUnit.SECONDS));
LOG.info("Reports updated.");
LOG.info("Test suite finished!");
}
private boolean waitForUpgrade(URI wsUri, Future<Session> response)
{
try
{
response.get(10, TimeUnit.SECONDS);
return true;
}
catch (Throwable t)
{
LOG.warn("Unable to connect to: " + wsUri, t);
return false;
}
}
}

View File

@ -0,0 +1,82 @@
//
// ========================================================================
// 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.websocket.tests.autobahn;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
/**
* WebSocket Server for use with <a href="https://github.com/crossbario/autobahn-testsuite">autobahn websocket testsuite</a> (wstest).
* <p>
* Installing Autobahn:
* </p>
* <pre>
* # For Debian / Ubuntu
* $ sudo apt-get install python python-dev python-twisted
* $ sudo apt-get install python-pip
* $ sudo pip install autobahntestsuite
*
* # For Fedora / Redhat
* $ sudo yum install python python-dev python-pip twisted
* $ sudo yum install libffi-devel
* $ sudo pip install autobahntestsuite
* </pre>
* <p>
* Upgrading an existing installation of autobahntestsuite
* </p>
* <pre>
* $ sudo pip install -U autobahntestsuite
* </pre>
* <p>
* Running Autobahn Fuzzing Client (against this server implementation):
* </p>
* <pre>
* # Change to jetty-websocket-tests directory first.
* $ cd jetty-websocket/jetty-websocket-tests/
* $ wstest --mode=fuzzingclient --spec=fuzzingclient.json
*
* # Report output is configured (in the fuzzingclient.json) at location:
* $ ls target/reports/servers/
* </pre>
*/
public class JettyAutobahnServer
{
public static void main(String[] args) throws Exception
{
int port = 9001; // same port as found in fuzzing-client.json
if (args != null && args.length > 0)
port = Integer.parseInt(args[0]);
Server server = new Server(port);
ServerConnector connector = new ServerConnector(server);
connector.setIdleTimeout(10000);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
server.setHandler(context);
JettyWebSocketServletContainerInitializer.configure(context, (servletContext, container)->
container.addMapping("/", (req, resp) -> new JettyAutobahnSocket()));
server.start();
server.join();
}
}

View File

@ -0,0 +1,37 @@
//
// ========================================================================
// 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.websocket.tests.autobahn;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.core.WebSocketConstants;
import org.eclipse.jetty.websocket.tests.EchoSocket;
@WebSocket
public class JettyAutobahnSocket extends EchoSocket
{
@Override
public void onOpen(Session session)
{
super.onOpen(session);
session.setMaxTextMessageSize(Long.MAX_VALUE);
session.setMaxBinaryMessageSize(Long.MAX_VALUE);
session.setMaxFrameSize(WebSocketConstants.DEFAULT_MAX_FRAME_SIZE*2);
}
}

View File

@ -141,7 +141,7 @@
</goals>
<configuration>
<!-- The class that contains a main method which accepts the port as a parameter to start the server. -->
<mainClass>org.eclipse.jetty.websocket.core.autobahn.AutobahnWebSocketServer</mainClass>
<mainClass>org.eclipse.jetty.websocket.core.autobahn.CoreAutobahnServer</mainClass>
</configuration>
</execution>
</executions>

View File

@ -28,7 +28,6 @@ import org.eclipse.jetty.util.DecoratedObjectFactory;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.ShutdownThread;
import org.eclipse.jetty.websocket.core.ExtensionConfig;
import org.eclipse.jetty.websocket.core.FrameHandler;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
@ -92,20 +91,9 @@ public class WebSocketCoreClient extends ContainerLifeCycle
if (LOG.isDebugEnabled())
LOG.debug("connect to websocket {}", request.getURI());
init();
return request.sendAsync();
}
// TODO: review need for this.
private synchronized void init() throws IOException
{
if (!ShutdownThread.isRegistered(this))
{
ShutdownThread.register(this);
}
}
public WebSocketExtensionRegistry getExtensionRegistry()
{
return components.getExtensionRegistry();

View File

@ -48,6 +48,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ -104,6 +105,7 @@ public class WebSocketNegotiationTest extends WebSocketTester
break;
case "test":
case "testExtensionThatDoesNotExist":
case "testInvalidExtensionParameter":
case "testAcceptTwoExtensionsOfSameName":
case "testInvalidUpgradeRequest":
@ -239,6 +241,23 @@ public class WebSocketNegotiationTest extends WebSocketTester
assertNull(extensionHeader.get(5, TimeUnit.SECONDS));
}
@Test
public void testExtensionThatDoesNotExist() throws Exception
{
Socket client = new Socket();
client.connect(new InetSocketAddress("127.0.0.1", server.getLocalPort()));
HttpFields httpFields = newUpgradeRequest("nonExistentExtensionName");
String upgradeRequest = "GET / HTTP/1.1\r\n" + httpFields;
client.getOutputStream().write(upgradeRequest.getBytes(StandardCharsets.ISO_8859_1));
String response = getUpgradeResponse(client.getInputStream());
assertThat(response, startsWith("HTTP/1.1 101 Switching Protocols"));
assertThat(response, containsString("Sec-WebSocket-Protocol: test"));
assertThat(response, containsString("Sec-WebSocket-Accept: +WahVcVmeMLKQUMm0fvPrjSjwzI="));
assertThat(response, not(containsString(HttpHeader.SEC_WEBSOCKET_EXTENSIONS.asString())));
}
@Test
public void testAcceptTwoExtensionsOfSameName() throws Exception
{

View File

@ -62,7 +62,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* Running Autobahn Fuzzing Server (which you run this client implementation against):
* </p>
* <pre>
* # Change to websocket-core
* # Change to websocket-core first
* $ cd jetty-websocket/websocket-core
* $ wstest --mode=fuzzingserver --spec=fuzzingserver.json
*
@ -70,7 +70,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* $ ls target/reports/clients/
* </pre>
*/
public class AutobahnWebSocketClient
public class CoreAutobahnClient
{
public static void main(String[] args)
{
@ -95,11 +95,11 @@ public class AutobahnWebSocketClient
}
}
AutobahnWebSocketClient client = null;
CoreAutobahnClient client = null;
try
{
String userAgent = "JettyWebsocketClient/" + Jetty.VERSION;
client = new AutobahnWebSocketClient(hostname, port, userAgent);
client = new CoreAutobahnClient(hostname, port, userAgent);
LOG.info("Running test suite...");
LOG.info("Using Fuzzing Server: {}:{}", hostname, port);
@ -137,12 +137,12 @@ public class AutobahnWebSocketClient
}
}
private static final Logger LOG = Log.getLogger(AutobahnWebSocketClient.class);
private static final Logger LOG = Log.getLogger(CoreAutobahnClient.class);
private URI baseWebsocketUri;
private WebSocketCoreClient client;
private String userAgent;
public AutobahnWebSocketClient(String hostname, int port, String userAgent) throws Exception
public CoreAutobahnClient(String hostname, int port, String userAgent) throws Exception
{
this.userAgent = userAgent;
this.baseWebsocketUri = new URI("ws://" + hostname + ":" + port);

View File

@ -57,7 +57,7 @@ import org.eclipse.jetty.websocket.core.server.WebSocketUpgradeHandler;
* $ ls target/reports/servers/
* </pre>
*/
public class AutobahnWebSocketServer
public class CoreAutobahnServer
{
public static void main(String[] args) throws Exception
{

View File

@ -176,7 +176,7 @@ public class ServletUpgradeResponse
if (matches < 1)
throw new IllegalArgumentException("Extension not a requested extension");
matches = negotiation.getNegotiatedExtensions().stream().filter(e -> e.getName().equals(config.getName())).count();
matches = configs.stream().filter(e -> e.getName().equals(config.getName())).count();
if (matches > 1)
throw new IllegalArgumentException("Multiple extensions of the same name");
}

View File

@ -988,7 +988,9 @@ public class HttpClientStreamTest extends AbstractTest<TransportScenario>
public void testUploadWithOutputStreamFailureToConnect(Transport transport) throws Exception
{
init(transport);
scenario.start(new EmptyServerHandler());
long connectTimeout = 1000;
scenario.start(new EmptyServerHandler(), httpClient -> httpClient.setConnectTimeout(connectTimeout));
final byte[] data = new byte[512];
final CountDownLatch latch = new CountDownLatch(1);
@ -1013,7 +1015,7 @@ public class HttpClientStreamTest extends AbstractTest<TransportScenario>
}
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(latch.await(2 * connectTimeout, TimeUnit.SECONDS));
}
@ParameterizedTest
@ -1070,7 +1072,9 @@ public class HttpClientStreamTest extends AbstractTest<TransportScenario>
public void testUploadWithConnectFailureClosesStream(Transport transport) throws Exception
{
init(transport);
scenario.start(new EmptyServerHandler());
long connectTimeout = 1000;
scenario.start(new EmptyServerHandler(), httpClient -> httpClient.setConnectTimeout(connectTimeout));
final CountDownLatch closeLatch = new CountDownLatch(1);
InputStream stream = new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))
@ -1097,7 +1101,7 @@ public class HttpClientStreamTest extends AbstractTest<TransportScenario>
completeLatch.countDown();
});
assertTrue(completeLatch.await(5, TimeUnit.SECONDS));
assertTrue(completeLatch.await(2 * connectTimeout, TimeUnit.SECONDS));
assertTrue(closeLatch.await(5, TimeUnit.SECONDS));
}