Working BrowserDebugTool for jetty websocket API

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Joakim Erdfelt 2019-01-14 10:13:59 -06:00
parent 74df6bf1a9
commit c320e020d2
9 changed files with 750 additions and 1 deletions

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.websocket.common;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.websocket.api.CloseStatus;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.SuspendToken;
@ -26,11 +27,12 @@ import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.core.FrameHandler;
import java.io.IOException;
import java.net.SocketAddress;
import java.time.Duration;
import java.util.Objects;
public class WebSocketSessionImpl implements Session
public class WebSocketSessionImpl implements Session, Dumpable
{
private final FrameHandler.CoreSession coreSession;
private final JettyWebSocketFrameHandler frameHandler;
@ -204,6 +206,21 @@ public class WebSocketSessionImpl implements Session
return null;
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
Dumpable.dumpObjects(out, indent, this, upgradeRequest, coreSession, remoteEndpoint, frameHandler);
}
@Override
public String dumpSelf()
{
return String.format("%s@%x[behavior=%s,idleTimeout=%d]",
this.getClass().getSimpleName(), hashCode(),
getPolicy().getBehavior(),
getIdleTimeout());
}
@Override
public String toString()
{

View File

@ -51,6 +51,11 @@
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,202 @@
//
// ========================================================================
// 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.server.browser;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.PathResource;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.websocket.core.ExtensionConfig;
import org.eclipse.jetty.websocket.core.internal.FrameCaptureExtension;
import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
/**
* Tool to help debug websocket circumstances reported around browsers.
* <p>
* Provides a server, with a few simple websocket's that can be twiddled from a browser. This helps with setting up breakpoints and whatnot to help debug our
* websocket implementation from the context of a browser client.
*/
public class BrowserDebugTool
{
private static final Logger LOG = Log.getLogger(BrowserDebugTool.class);
public static void main(String[] args)
{
int port = 8080;
for (int i = 0; i < args.length; i++)
{
String a = args[i];
if ("-p".equals(a) || "--port".equals(a))
{
port = Integer.parseInt(args[++i]);
}
}
try
{
BrowserDebugTool tool = new BrowserDebugTool();
tool.prepare(port);
tool.start();
}
catch (Throwable t)
{
LOG.warn(t);
}
}
private Server server;
private ServerConnector connector;
public int getPort()
{
return connector.getLocalPort();
}
public void prepare(int port) throws IOException, URISyntaxException {
server = new Server();
connector = new ServerConnector(server);
connector.setPort(port);
server.addConnector(connector);
ServletContextHandler context = new ServletContextHandler();
JettyWebSocketServletContainerInitializer.configure(context);
context.setContextPath("/");
Resource staticResourceBase = findStaticResources();
context.setBaseResource(staticResourceBase);
context.addServlet(BrowserSocketServlet.class, "/*");
ServletHolder defHolder = new ServletHolder("default", DefaultServlet.class);
context.addServlet(defHolder, "/");
HandlerList handlers = new HandlerList();
handlers.addHandler(context);
handlers.addHandler(new DefaultHandler());
server.setHandler(handlers);
LOG.info("{} setup on port {}",this.getClass().getName(),port);
}
private Resource findStaticResources()
{
Path path = MavenTestingUtils.getTestResourcePathDir("browser-debug-tool");
LOG.info("Static Resources: {}", path);
return new PathResource(path);
}
public void start() throws Exception
{
server.start();
LOG.info("Server available on port {}",getPort());
}
public void stop() throws Exception
{
server.stop();
}
public static class BrowserSocketServlet extends WebSocketServlet
{
@Override
public void configure(WebSocketServletFactory factory) {
LOG.debug("Configuring WebSocketServerFactory ...");
// Registering Frame Debug
factory.getExtensionRegistry().register("@frame-capture", FrameCaptureExtension.class);
// Setup the desired Socket to use for all incoming upgrade requests
factory.addMapping(new ServletPathSpec("/"), new BrowserSocketCreator());
// Set the timeout
factory.setDefaultIdleTimeout(Duration.ofSeconds(30));
// Set top end message size
factory.setDefaultMaxTextMessageSize(15 * 1024 * 1024);
}
}
public static class BrowserSocketCreator implements WebSocketCreator
{
@Override
public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
LOG.debug("Creating BrowserSocket");
if (req.getSubProtocols() != null)
{
if (!req.getSubProtocols().isEmpty())
{
String subProtocol = req.getSubProtocols().get(0);
resp.setAcceptedSubProtocol(subProtocol);
}
}
String ua = req.getHeader("User-Agent");
String rexts = req.getHeader("Sec-WebSocket-Extensions");
// manually negotiate extensions
List<ExtensionConfig> negotiated = new ArrayList<>();
// adding frame debug
negotiated.add(new ExtensionConfig("@frame-capture; output-dir=target"));
for (ExtensionConfig config : req.getExtensions())
{
if (config.getName().equals("permessage-deflate"))
{
// what we are interested in here
negotiated.add(config);
continue;
}
// skip all others
}
resp.setExtensions(negotiated);
LOG.debug("User-Agent: {}",ua);
LOG.debug("Sec-WebSocket-Extensions (Request) : {}",rexts);
LOG.debug("Sec-WebSocket-Protocol (Request): {}",req.getHeader("Sec-WebSocket-Protocol"));
LOG.debug("Sec-WebSocket-Protocol (Response): {}",resp.getAcceptedSubProtocol());
req.getExtensions();
return new BrowserSocket(ua,rexts);
}
}
}

View File

@ -0,0 +1,291 @@
//
// ========================================================================
// 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.server.browser;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.Random;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@WebSocket
public class BrowserSocket
{
private static class WriteMany implements Runnable
{
private RemoteEndpoint remote;
private int size;
private int count;
public WriteMany(RemoteEndpoint remote, int size, int count)
{
this.remote = remote;
this.size = size;
this.count = count;
}
@Override
public void run()
{
char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-|{}[]():".toCharArray();
int lettersLen = letters.length;
char randomText[] = new char[size];
Random rand = new Random(42);
String msg;
for (int n = 0; n < count; n++)
{
// create random text
for (int i = 0; i < size; i++)
{
randomText[i] = letters[rand.nextInt(lettersLen)];
}
msg = String.format("ManyThreads [%s]",String.valueOf(randomText));
remote.sendString(msg,null);
}
}
}
private static final Logger LOG = Log.getLogger(BrowserSocket.class);
private Session session;
private final String userAgent;
private final String requestedExtensions;
public BrowserSocket(String ua, String reqExts)
{
this.userAgent = ua;
this.requestedExtensions = reqExts;
}
@OnWebSocketConnect
public void onConnect(Session session)
{
LOG.info("Connect [{}]",session);
this.session = session;
}
@OnWebSocketClose
public void onDisconnect(int statusCode, String reason)
{
this.session = null;
LOG.info("Closed [{}, {}]",statusCode,reason);
}
@OnWebSocketError
public void onError(Throwable cause)
{
this.session = null;
LOG.warn("Error",cause);
}
@OnWebSocketMessage
public void onTextMessage(String message)
{
if (message.length() > 300)
{
int len = message.length();
LOG.info("onTextMessage({} ... {}) size:{}",message.substring(0,15),message.substring(len - 15,len).replaceAll("[\r\n]*",""),len);
}
else
{
LOG.info("onTextMessage({})",message);
}
// Is multi-line?
if (message.contains("\n"))
{
// echo back exactly
writeMessage(message);
return;
}
// Is resource lookup?
if (message.charAt(0) == '@')
{
String name = message.substring(1);
URL url = Loader.getResource(name);
if (url == null)
{
writeMessage("Unable to find resource: " + name);
return;
}
try (InputStream in = url.openStream())
{
String data = IO.toString(in);
writeMessage(data);
}
catch (IOException e)
{
writeMessage("Unable to read resource: " + name);
LOG.warn("Unable to read resource: " + name,e);
}
return;
}
// Is parameterized?
int idx = message.indexOf(':');
if (idx > 0)
{
String key = message.substring(0,idx).toLowerCase(Locale.ENGLISH);
String val = message.substring(idx + 1);
switch (key)
{
case "info":
{
if (StringUtil.isBlank(userAgent))
{
writeMessage("Client has no User-Agent");
}
else
{
writeMessage("Client User-Agent: " + this.userAgent);
}
if (StringUtil.isBlank(requestedExtensions))
{
writeMessage("Client requested no Sec-WebSocket-Extensions");
}
else
{
writeMessage("Client requested Sec-WebSocket-Extensions: " + this.requestedExtensions);
writeMessage("Negotiated Sec-WebSocket-Extensions: " + session.getUpgradeResponse().getHeader("Sec-WebSocket-Extensions"));
}
break;
}
case "many":
{
String parts[] = val.split(",");
int size = Integer.parseInt(parts[0]);
int count = Integer.parseInt(parts[1]);
writeManyAsync(size,count);
break;
}
case "manythreads":
{
String parts[] = val.split(",");
int threadCount = Integer.parseInt(parts[0]);
int size = Integer.parseInt(parts[1]);
int count = Integer.parseInt(parts[2]);
Thread threads[] = new Thread[threadCount];
// Setup threads
for (int n = 0; n < threadCount; n++)
{
threads[n] = new Thread(new WriteMany(session.getRemote(),size,count),"WriteMany[" + n + "]");
}
// Execute threads
for (Thread thread : threads)
{
thread.start();
}
// Drop out of this thread
break;
}
case "time":
{
Calendar now = Calendar.getInstance();
DateFormat sdf = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.FULL,SimpleDateFormat.FULL);
writeMessage("Server time: %s",sdf.format(now.getTime()));
break;
}
case "dump":
{
if(session instanceof Dumpable) {
System.err.println(((Dumpable) session).dump());
} else {
System.err.println(session.toString());
}
break;
}
default:
{
writeMessage("key[%s] val[%s]",key,val);
}
}
return;
}
// Not parameterized, echo it back as-is
writeMessage(message);
}
private void writeManyAsync(int size, int count)
{
char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-|{}[]():".toCharArray();
int lettersLen = letters.length;
char randomText[] = new char[size];
Random rand = new Random(42);
for (int n = 0; n < count; n++)
{
// create random text
for (int i = 0; i < size; i++)
{
randomText[i] = letters[rand.nextInt(lettersLen)];
}
writeMessage("Many [%s]",String.valueOf(randomText));
}
}
private void writeMessage(String message)
{
if (this.session == null)
{
LOG.debug("Not connected");
return;
}
if (!session.isOpen())
{
LOG.debug("Not open");
return;
}
// Async write
session.getRemote().sendString(message,null);
}
private void writeMessage(String format, Object... args)
{
writeMessage(String.format(format,args));
}
}

View File

@ -0,0 +1,58 @@
<html>
<head>
<title>Jetty WebSocket Browser -&gt; Server Debug Tool</title>
<script type="text/javascript" src="websocket.js"></script>
<link rel="stylesheet" type="text/css" href="main.css" media="all" >
</head>
<body>
jetty websocket/browser/javascript -&gt; server debug tool #console
<div id="console"></div>
<div id="buttons">
<input id="connect" class="button" type="submit" name="connect" value="connect"/>
<input id="close" class="button" type="submit" name="close" value="close" disabled="disabled"/>
<input id="info" class="button" type="submit" name="info" value="info" disabled="disabled"/>
<input id="time" class="button" type="submit" name="time" value="time" disabled="disabled"/>
<input id="many" class="button" type="submit" name="many" value="many" disabled="disabled"/>
<input id="manythreads" class="button" type="submit" name="many" value="manythreads" disabled="disabled"/>
<input id="hello" class="button" type="submit" name="hello" value="hello" disabled="disabled"/>
<input id="there" class="button" type="submit" name="there" value="there" disabled="disabled"/>
<input id="dump" class="button" type="submit" name="dump" value="dump" disabled="disabled"/>
<input id="json" class="button" type="submit" name="json" value="json" disabled="disabled"/>
<input id="send10k" class="button" type="submit" name="send10k" value="send10k" disabled="disabled"/>
<input id="send100k" class="button" type="submit" name="send100k" value="send100k" disabled="disabled"/>
<input id="send1000k" class="button" type="submit" name="send1000k" value="send1000k" disabled="disabled"/>
<input id="send10m" class="button" type="submit" name="send10m" value="send10m" disabled="disabled"/>
</div>
<script type="text/javascript">
$("connect").onclick = function(event) { wstool.connect(); return false; }
$("close").onclick = function(event) {wstool.close(); return false; }
$("info").onclick = function(event) {wstool.write("info:"); return false; }
$("time").onclick = function(event) {wstool.write("time:"); return false; }
$("many").onclick = function(event) {wstool.write("many:15,300"); return false; }
$("manythreads").onclick = function(event) {wstool.write("manythreads:20,25,60"); return false; }
$("hello").onclick = function(event) {wstool.write("Hello"); return false; }
$("there").onclick = function(event) {wstool.write("There"); return false; }
$("dump").onclick = function(event) {wstool.write("dump:"); return false; }
$("json").onclick = function(event) {wstool.write("[{\"channel\":\"/meta/subscribe\",\"subscription\":\"/chat/demo\",\"id\":\"2\",\"clientId\":\"81dwnxwbgs0h0bq8968b0a0gyl\",\"timestamp\":\"Thu,"
+ " 12 Sep 2013 19:42:30 GMT\"},{\"channel\":\"/meta/subscribe\",\"subscription\":\"/members/demo\",\"id\":\"3\",\"clientId\":\"81dwnxwbgs0h0bq8968b0a0gyl\",\"timestamp\":\"Thu,"
+ " 12 Sep 2013 19:42:30 GMT\"},{\"channel\":\"/chat/demo\",\"data\":{\"user\":\"ch\",\"membership\":\"join\",\"chat\":\"ch"
+ " has joined\"},\"id\":\"4\",\"clientId\":\"81dwnxwbgs0h0bq8968b0a0gyl\",\"timestamp\":\"Thu,"
+ " 12 Sep 2013 19:42:30 GMT\"}]"); return false; }
$("send10k").onclick = function(event) {wstool.write(randomString( 10*1024)); return false;}
$("send100k").onclick = function(event) {wstool.write(randomString( 100*1024)); return false;}
$("send1000k").onclick = function(event) {wstool.write(randomString(1000*1024)); return false;}
$("send10m").onclick = function(event) {wstool.write(randomString( 10*1024*1024)); return false;}
function randomString(len, charSet) {
charSet = charSet || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{} ":;<>,.()[]';
var randomString = '';
var charLen = charSet.length;
for (var i = 0; i < len; i++) {
var randomPoz = Math.floor(Math.random() * charLen);
randomString += charSet.substring(randomPoz,randomPoz+1);
}
return randomString;
}
</script>
</body>
</html>

View File

@ -0,0 +1,29 @@
body {
font-family: sans-serif;
}
div {
border: 0px solid black;
}
div#console {
clear: both;
width: 40em;
height: 20em;
overflow: auto;
background-color: #f0f0f0;
padding: 4px;
border: 1px solid black;
}
div#console .info {
color: black;
}
div#console .client {
color: blue;
}
div#console .server {
color: magenta;
}

View File

@ -0,0 +1,141 @@
if (!window.WebSocket && window.MozWebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (!window.WebSocket) {
alert("WebSocket not supported by this browser");
}
function $() {
return document.getElementById(arguments[0]);
}
function $F() {
return document.getElementById(arguments[0]).value;
}
function getKeyCode(ev) {
if (window.event)
return window.event.keyCode;
return ev.keyCode;
}
var wstool = {
connect : function() {
var location = document.location.toString().replace('http://', 'ws://')
.replace('https://', 'wss://');
wstool.info("Document URI: " + document.location);
wstool.info("WS URI: " + location);
this._scount = 0;
try {
this._ws = new WebSocket(location, "tool");
this._ws.onopen = this._onopen;
this._ws.onmessage = this._onmessage;
this._ws.onclose = this._onclose;
} catch (exception) {
wstool.info("Connect Error: " + exception);
}
},
close : function() {
this._ws.close();
},
_out : function(css, message) {
var console = $('console');
var spanText = document.createElement('span');
spanText.className = 'text ' + css;
spanText.innerHTML = message;
var lineBreak = document.createElement('br');
console.appendChild(spanText);
console.appendChild(lineBreak);
console.scrollTop = console.scrollHeight - console.clientHeight;
},
info : function(message) {
wstool._out("info", message);
},
infoc : function(message) {
if(message.length > 300) {
wstool._out("client", "[c] [big message: " + message.length + " characters]");
} else {
wstool._out("client", "[c] " + message);
}
},
infos : function(message) {
this._scount++;
if(message.length > 300) {
wstool._out("server", "[s" + this._scount + "] [big message: " + message.length + " characters]");
} else {
wstool._out("server", "[s" + this._scount + "] " + message);
}
},
setState : function(enabled) {
$('connect').disabled = enabled;
$('close').disabled = !enabled;
$('info').disabled = !enabled;
$('time').disabled = !enabled;
$('many').disabled = !enabled;
$('manythreads').disabled = !enabled;
$('hello').disabled = !enabled;
$('there').disabled = !enabled;
$('dump').disabled = !enabled;
$('json').disabled = !enabled;
$('send10k').disabled = !enabled;
$('send100k').disabled = !enabled;
$('send1000k').disabled = !enabled;
$('send10m').disabled = !enabled;
},
_onopen : function() {
wstool.setState(true);
wstool.info("Websocket Connected");
},
_send : function(message) {
if (this._ws) {
this._ws.send(message);
wstool.infoc(message);
}
},
write : function(text) {
wstool._send(text);
},
_onmessage : function(m) {
if (m.data) {
wstool.infos(m.data);
}
},
_onclose : function(closeEvent) {
this._ws = null;
wstool.setState(false);
wstool.info("Websocket Closed");
wstool.info(" .wasClean = " + closeEvent.wasClean);
var codeMap = {};
codeMap[1000] = "(NORMAL)";
codeMap[1001] = "(ENDPOINT_GOING_AWAY)";
codeMap[1002] = "(PROTOCOL_ERROR)";
codeMap[1003] = "(UNSUPPORTED_DATA)";
codeMap[1004] = "(UNUSED/RESERVED)";
codeMap[1005] = "(INTERNAL/NO_CODE_PRESENT)";
codeMap[1006] = "(INTERNAL/ABNORMAL_CLOSE)";
codeMap[1007] = "(BAD_DATA)";
codeMap[1008] = "(POLICY_VIOLATION)";
codeMap[1009] = "(MESSAGE_TOO_BIG)";
codeMap[1010] = "(HANDSHAKE/EXT_FAILURE)";
codeMap[1011] = "(SERVER/UNEXPECTED_CONDITION)";
codeMap[1015] = "(INTERNAL/TLS_ERROR)";
var codeStr = codeMap[closeEvent.code];
wstool.info(" .code = " + closeEvent.code + " " + codeStr);
wstool.info(" .reason = " + closeEvent.reason);
}
};

View File

@ -19,6 +19,7 @@
package org.eclipse.jetty.websocket.core.internal;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
@ -111,12 +112,14 @@ public class FrameCaptureExtension extends AbstractExtension
}
ByteBuffer buf = getBufferPool().acquire(BUFSIZE, false);
BufferUtil.flipToFill(buf);
try
{
Frame f = Frame.copy(frame);
f.setMask(null); // TODO is this needed?
generator.generateHeaderBytes(f, buf);
BufferUtil.flipToFlush(buf, 0);
channel.write(buf);
if (frame.hasPayload())
{

View File

@ -19,6 +19,7 @@
package org.eclipse.jetty.websocket.servlet;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry;
import java.time.Duration;
@ -40,6 +41,8 @@ public interface WebSocketServletFactory
*/
void addMapping(PathSpec pathSpec, WebSocketCreator creator);
WebSocketExtensionRegistry getExtensionRegistry();
Duration getDefaultIdleTimeout();
void setDefaultIdleTimeout(Duration duration);