org.glassfish.tyrus.servlet.TyrusHttpUpgradeHandler Maven / Gradle / Ivy
/*
* Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.tyrus.servlet;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import java.util.logging.Logger;
import jakarta.websocket.CloseReason;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpUpgradeHandler;
import jakarta.servlet.http.WebConnection;
import org.glassfish.tyrus.core.CloseReasons;
import org.glassfish.tyrus.spi.Connection;
import org.glassfish.tyrus.spi.WebSocketEngine;
import org.glassfish.tyrus.spi.Writer;
/**
* {@link HttpUpgradeHandler} and {@link ReadListener} implementation.
*
* Reads data from {@link ServletInputStream} and passes it further to the Tyrus runtime.
*
* @author Jitendra Kotamraju
* @author Pavel Bucek
*/
public class TyrusHttpUpgradeHandler implements HttpUpgradeHandler, ReadListener {
/**
*
* The size to precede OutOfMemory Exception and potentially DDoS attacks when buffering incoming WebSocket frames.
*
*
* The default value is 4194315 bytes, which correspond to 4M plus few bytes to frame headers.
*
*/
public static final String FRAME_BUFFER_SIZE = "org.glassfish.tyrus.servlet.incoming-buffer-size";
private final CountDownLatch connectionLatch = new CountDownLatch(1);
private ServletInputStream is;
private ServletOutputStream os;
private WebConnection wc;
private ByteBuffer buf;
private volatile boolean closed = false;
private int incomingBufferSize = 4194315; // 4M (payload) + 11 (frame overhead)
private static final Logger LOGGER = Logger.getLogger(TyrusHttpUpgradeHandler.class.getName());
private Connection connection;
private WebSocketEngine.UpgradeInfo upgradeInfo;
private Writer writer;
private boolean authenticated = false;
@Override
public void init(WebConnection wc) {
LOGGER.config("Servlet 3.1 Upgrade");
try {
is = wc.getInputStream();
os = wc.getOutputStream();
this.wc = wc;
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
try {
is.setReadListener(this);
} catch (IllegalStateException e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
}
connection = upgradeInfo.createConnection(writer, new Connection.CloseListener() {
@Override
public void close(CloseReason reason) {
try {
TyrusHttpUpgradeHandler.this.getWebConnection().close();
} catch (Exception e) {
LOGGER.log(Level.FINE, e.getMessage(), e);
}
}
});
connectionLatch.countDown();
}
/**
* Sets the required information before {@link #init(WebConnection)} is invoked.
* @param upgradeInfo The WebSocket UpgradeInfo.
* @param writer The Tyrus SPI Writer.
* @param authenticated Whether the authentication has been used.
*/
public void preInit(WebSocketEngine.UpgradeInfo upgradeInfo, Writer writer, boolean authenticated) {
this.upgradeInfo = upgradeInfo;
this.writer = writer;
this.authenticated = authenticated;
}
@Override
public void onDataAvailable() {
try {
connectionLatch.await();
} catch (InterruptedException e) {
// do nothing.
}
do {
try {
int available = is.available();
if (available == 0) {
// tomcat impl returns always 0
available = 16384;
}
int toRead;
if (buf == null) {
if (available > incomingBufferSize) {
toRead = incomingBufferSize;
} else {
toRead = available;
}
} else {
if (buf.remaining() + available > incomingBufferSize) {
toRead = incomingBufferSize - buf.remaining();
} else {
toRead = buf.remaining() + available;
}
}
if (toRead == 0) {
throw new IOException(String.format("Tyrus input buffer exceeded. Current buffer size is %s bytes.",
incomingBufferSize));
}
available -= fillBuf(toRead);
if (buf != null) {
LOGGER.finest(String.format("Remaining Data = %d", buf.remaining()));
if (buf.hasRemaining()) {
connection.getReadHandler().handle(buf);
}
}
} catch (IOException e) {
connection.close(CloseReasons.CANNOT_ACCEPT.getCloseReason());
}
} while (!closed && is.isReady());
}
/**
* Fill the buf with some more websocket protocol data.
*
* @param length length of data available to read.
* @return legth of actually read data.
* @throws IOException if some other I/O error occurs.
*/
private int fillBuf(int length) throws IOException {
byte[] data = new byte[length];
int len = is.read(data);
if (len == 0) {
return 0;
}
if (buf == null) {
LOGGER.finest("No Buffer. Allocating new one");
buf = ByteBuffer.wrap(data);
((Buffer) buf).limit(len);
} else {
int limit = buf.limit();
int capacity = buf.capacity();
int remaining = buf.remaining();
if (capacity - limit >= len) {
// Remaining data need not be changed. New data is just appended
LOGGER.finest("Remaining data need not be moved. New data is just appended");
((Buffer) buf).mark();
((Buffer) buf).position(limit);
((Buffer) buf).limit(capacity);
buf.put(data, 0, len);
((Buffer) buf).limit(limit + len);
((Buffer) buf).reset();
} else if (remaining + len < capacity) {
// Remaining data is moved to left. Then new data is appended
LOGGER.finest("Remaining data is moved to left. Then new data is appended");
buf.compact();
buf.put(data, 0, len);
((Buffer) buf).flip();
} else {
// Remaining data + new > capacity. So allocate new one
LOGGER.finest("Remaining data + new > capacity. So allocate new one");
byte[] array = new byte[remaining + len];
buf.get(array, 0, remaining);
System.arraycopy(data, 0, array, remaining, len);
buf = ByteBuffer.wrap(array);
((Buffer) buf).limit(remaining + len);
}
}
return len;
}
@Override
public void onAllDataRead() {
close(CloseReason.CloseCodes.NORMAL_CLOSURE.getCode(), null);
}
@Override
public void onError(Throwable t) {
close(CloseReason.CloseCodes.CLOSED_ABNORMALLY.getCode(),
t.getMessage() == null ? "No reason given." : t.getMessage());
}
@Override
public void destroy() {
close(CloseReason.CloseCodes.CLOSED_ABNORMALLY.getCode(), "No reason given.");
}
/**
* Called when related {@link jakarta.servlet.http.HttpSession} is destroyed or invalidated.
*
* Implementation is required to call onClose() on server-side with corresponding close code (1008, see
* WebSocket spec 7.2) - only when there is an authorized user for this session.
*/
public void sessionDestroyed() {
if (authenticated) {
// websocket spec 7.2 [WSC-7.2-3]
httpSessionForcedClose(CloseReason.CloseCodes.VIOLATED_POLICY.getCode(), "No reason given.");
}
// else do nothing.
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("TyrusHttpUpgradeHandler{");
sb.append("is=").append(is);
sb.append(", os=").append(os);
sb.append(", wc=").append(wc);
sb.append(", closed=").append(closed);
sb.append('}');
return sb.toString();
}
/**
* Override the default {@link #FRAME_BUFFER_SIZE}.
* @param incomingBufferSize The new incoming frame buffer size value.
*/
public void setIncomingBufferSize(int incomingBufferSize) {
this.incomingBufferSize = incomingBufferSize;
}
private void httpSessionForcedClose(int closeCode, String closeReason) {
if (!closed) {
try {
// TODO
// initiates connection close without sending close frame to the client - session is already invalidated
// so we should not send anything.
// ((TyrusWebSocket) ((TyrusWebSocketEngine) engine).getWebSocketHolder(writer).webSocket).setClosed();
connection.close(new CloseReason(CloseReason.CloseCodes.getCloseCode(closeCode), closeReason));
closed = true;
wc.close();
} catch (Exception e) {
LOGGER.log(Level.CONFIG, e.getMessage(), e);
}
}
}
private void close(int closeCode, String closeReason) {
if (!closed) {
try {
connection.close(CloseReasons.create(CloseReason.CloseCodes.getCloseCode(closeCode), closeReason));
closed = true;
wc.close();
} catch (Exception e) {
LOGGER.log(Level.CONFIG, e.getMessage(), e);
}
}
}
WebConnection getWebConnection() {
if (wc == null) {
throw new IllegalStateException();
}
return wc;
}
}