
org.red5.net.websocket.WebSocketConnection Maven / Gradle / Ivy
/*
* RED5 Open Source Flash Server - https://github.com/red5 Copyright 2006-2018 by respective authors (see below). All rights reserved. Licensed under the Apache License, Version
* 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless
* required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
package org.red5.net.websocket;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.websocket.Constants;
import org.apache.tomcat.websocket.WsSession;
import org.red5.server.AttributeStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.websocket.CloseReason;
import jakarta.websocket.CloseReason.CloseCode;
import jakarta.websocket.CloseReason.CloseCodes;
import jakarta.websocket.Extension;
import jakarta.websocket.Session;
/**
* WebSocketConnection
* This class represents a WebSocket connection with a client (browser).
*
* @see rfc6455
*
* @author Paul Gregoire
*/
public class WebSocketConnection extends AttributeStore implements Comparable {
private static final Logger log = LoggerFactory.getLogger(WebSocketConnection.class);
private static final boolean isTrace = log.isTraceEnabled();
private static final boolean isDebug = log.isDebugEnabled();
// Sending async on windows times out
private static boolean useAsync;
private static long sendTimeout = 8000L, readTimeout = 30000L;
private static final AtomicLongFieldUpdater readBytesUpdater = AtomicLongFieldUpdater.newUpdater(WebSocketConnection.class, "readBytes");
private static final AtomicLongFieldUpdater writeBytesUpdater = AtomicLongFieldUpdater.newUpdater(WebSocketConnection.class, "writtenBytes");
private AtomicBoolean connected = new AtomicBoolean(false);
// associated websocket session
private final WsSession wsSession;
// reference to the scope for manager access
private WeakReference scope;
// unique identifier for the session
private final String wsSessionId;
// unique identifier for this instance based upon the websocket session id
private final int hashCode;
private String host;
private String path;
private String origin;
private String userAgent = "undefined";
/**
* Contains http headers and other web-socket information from the initial request.
*/
private Map> headers;
private Map extensions = new HashMap<>();
/**
* Contains uri parameters from the initial request.
*/
private Map querystringParameters = new HashMap<>();
/**
* Connection protocol (ex. chat, json, etc)
*/
private String protocol;
// stats
private volatile long readBytes, writtenBytes;
// send future for when async is enabled
private Future sendFuture;
public WebSocketConnection(WebSocketScope scope, Session session) {
log.debug("New WebSocket - scope: {} session: {}", scope, session);
// set the scope for ease of use later
this.scope = new WeakReference<>(scope);
// set our path
path = scope.getPath();
if (isDebug) {
log.debug("path: {}", path);
}
// cast ws session
this.wsSession = (WsSession) session;
if (isDebug) {
log.debug("ws session: {}", wsSession);
}
// the websocket session id will be used for hash code comparison, its the only usable value currently
wsSessionId = session.getId();
if (isDebug) {
log.debug("wsSessionId: {}", wsSessionId);
}
hashCode = wsSessionId.hashCode();
log.info("ws id: {} hashCode: {}", wsSessionId, hashCode);
// get extensions
List extList = session.getNegotiatedExtensions();
if (extList != null) {
extList.forEach(extension -> {
extensions.put(extension.getName(), extension);
});
}
if (isDebug) {
log.debug("extensions: {}", extensions);
}
// get querystring
String queryString = session.getQueryString();
if (isDebug) {
log.debug("queryString: {}", queryString);
}
if (StringUtils.isNotBlank(queryString)) {
// bust it up by ampersand
String[] qsParams = queryString.split("&");
// loop-thru adding to the local map
Stream.of(qsParams).forEach(qsParam -> {
String[] parts = qsParam.split("=");
if (parts.length == 2) {
querystringParameters.put(parts[0], parts[1]);
} else {
querystringParameters.put(parts[0], null);
}
});
}
// get request parameters
Map pathParameters = session.getPathParameters();
if (isDebug) {
log.debug("pathParameters: {}", pathParameters);
}
// get user props
Map userProps = session.getUserProperties();
// add the timeouts to the user props
userProps.put(Constants.READ_IDLE_TIMEOUT_MS, readTimeout);
userProps.put(Constants.WRITE_IDLE_TIMEOUT_MS, sendTimeout);
// set the close timeout to 5 seconds
userProps.put(Constants.SESSION_CLOSE_TIMEOUT_PROPERTY, TimeUnit.SECONDS.toMillis(5));
if (isDebug) {
log.debug("userProps: {}", userProps);
}
// set maximum messages size to 10,000 bytes
session.setMaxTextMessageBufferSize(10000);
// set maximum idle timeout to 30 seconds (read timeout)
session.setMaxIdleTimeout(readTimeout);
}
/**
* Sends text to the client.
*
* @param data
* string / text data
* @throws UnsupportedEncodingException
* @throws IOException
*/
public void send(String data) throws UnsupportedEncodingException, IOException {
if (isDebug) {
log.debug("send message: {}", data);
}
// process the incoming string
if (StringUtils.isNotBlank(data)) {
// attempt send only if the session is not closed
if (!wsSession.isClosed()) {
try {
if (useAsync) {
if (sendFuture != null && !sendFuture.isDone()) {
try {
sendFuture.get(sendTimeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
log.warn("Send timed out {}", wsSessionId);
// if the session is not open, cancel the future
if (!wsSession.isOpen()) {
sendFuture.cancel(true);
return;
}
}
}
synchronized (wsSessionId) {
int lengthToWrite = data.getBytes().length;
sendFuture = wsSession.getAsyncRemote().sendText(data);
updateWriteBytes(lengthToWrite);
}
} else {
synchronized (wsSessionId) {
int lengthToWrite = data.getBytes().length;
wsSession.getBasicRemote().sendText(data);
updateWriteBytes(lengthToWrite);
}
}
} catch (Exception e) {
log.warn("Send text exception", e);
}
} else {
throw new IOException("WS session closed");
}
} else {
throw new UnsupportedEncodingException("Cannot send a null string");
}
}
/**
* Sends binary data to the client.
*
* @param buf
* @throws IOException
*/
public void send(byte[] buf) throws IOException {
if (isDebug) {
log.debug("send binary: {}", Arrays.toString(buf));
}
if (!wsSession.isClosed()) {
try {
// send the bytes
if (useAsync) {
if (sendFuture != null && !sendFuture.isDone()) {
try {
sendFuture.get(sendTimeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
log.warn("Send timed out {}", wsSessionId);
if (!isConnected()) {
sendFuture.cancel(true);
return;
}
}
}
synchronized (wsSessionId) {
sendFuture = wsSession.getAsyncRemote().sendBinary(ByteBuffer.wrap(buf));
updateWriteBytes(buf.length);
}
} else {
synchronized (wsSessionId) {
wsSession.getBasicRemote().sendBinary(ByteBuffer.wrap(buf));
updateWriteBytes(buf.length);
}
}
} catch (Exception e) {
log.warn("Send bytes exception", e);
}
} else {
throw new IOException("WS session closed");
}
}
/**
* Sends a ping to the client.
*
* @param buf
* @throws IOException
* @throws IllegalArgumentException
*/
public void sendPing(byte[] buf) throws IllegalArgumentException, IOException {
if (isTrace) {
log.trace("send ping: {}", buf);
}
if (!wsSession.isClosed()) {
synchronized (wsSessionId) {
// send the bytes
wsSession.getBasicRemote().sendPing(ByteBuffer.wrap(buf));
// update counter
updateWriteBytes(buf.length);
}
} else {
throw new IOException("WS session closed");
}
}
/**
* Sends a pong back to the client; normally in response to a ping.
*
* @param buf
* @throws IOException
* @throws IllegalArgumentException
*/
public void sendPong(byte[] buf) throws IllegalArgumentException, IOException {
if (isTrace) {
log.trace("send pong: {}", buf);
}
if (!wsSession.isClosed()) {
synchronized (wsSessionId) {
// send the bytes
wsSession.getBasicRemote().sendPong(ByteBuffer.wrap(buf));
// update counter
updateWriteBytes(buf.length);
}
} else {
throw new IOException("WS session closed");
}
}
/**
* Close the connection.
*/
public void close() {
close(CloseCodes.NORMAL_CLOSURE, "");
}
/**
* Close the connection with a reason.
*
* @param code CloseCode
* @param reasonPhrase short reason for closing
*/
public void close(CloseCode code, String reasonPhrase) {
if (connected.compareAndSet(true, false)) {
// no blank reasons
if (reasonPhrase == null) {
reasonPhrase = "";
}
log.debug("close: {} code: {} reason: {}", wsSessionId, code, reasonPhrase);
try {
// close the session if open
if (wsSession.isOpen()) {
CloseReason reason = new CloseReason(code, reasonPhrase);
if (isDebug) {
log.debug("Closing session: {} with reason: {}", wsSessionId, reason);
}
wsSession.close(reason);
}
} catch (Exception e) {
log.debug("Exception closing session", e);
}
// clean up our props
attributes.clear();
if (querystringParameters != null) {
querystringParameters.clear();
querystringParameters = null;
}
if (extensions != null) {
extensions.clear();
extensions = null;
}
if (headers != null) {
headers = null;
}
}
}
/**
* Async send is enabled in non-Windows based systems; this provides a means to override it.
*
* @param useAsync
*/
public static void setUseAsync(boolean useAsync) {
if (!useAsync) {
log.debug("Async websocket sends are disabled");
}
WebSocketConnection.useAsync = useAsync;
}
/**
* Return the WebSocketScope to which we're connected/connecting.
*
* @return WebSocketScope
*/
public WebSocketScope getScope() {
return scope != null ? scope.get() : null;
}
/**
* @return the connected
*/
public boolean isConnected() {
return connected.get();
}
/**
* On connected, set flag.
*/
public void setConnected() {
boolean connectSuccess = connected.compareAndSet(false, true);
log.debug("Connect success: {}", connectSuccess);
}
/**
* @return the host
*/
public String getHost() {
return String.format("%s://%s%s", (isSecure() ? "wss" : "ws"), host, path);
}
/**
* @param host
* the host to set
*/
public void setHost(String host) {
this.host = host;
}
/**
* @return the origin
*/
public String getOrigin() {
return origin;
}
/**
* @param origin
* the origin to set
*/
public void setOrigin(String origin) {
this.origin = origin;
}
/**
* Return whether or not the session is secure.
*
* @return true if secure and false if unsecure or unconnected
*/
public boolean isSecure() {
Optional opt = Optional.ofNullable(wsSession);
if (opt.isPresent()) {
return (opt.get().isOpen() ? opt.get().isSecure() : false);
}
return false;
}
public String getPath() {
return path;
}
/**
* @param path
* the path to set
*/
public void setPath(String path) {
if (path.charAt(path.length() - 1) == '/') {
this.path = path.substring(0, path.length() - 1);
} else {
this.path = path;
}
}
/**
* Returns the WsSession id associated with this connection.
*
* @return sessionId
*/
public String getSessionId() {
return wsSessionId;
}
/**
* Sets / overrides this connections HttpSession id.
*
* @param httpSessionId
* @deprecated Session id read from WSSession
*/
@Deprecated(since = "1.2.26")
public void setHttpSessionId(String httpSessionId) {
//this.httpSessionId = httpSessionId;
}
/**
* Returns the HttpSession id associated with this connection.
*
* @return sessionId
* @deprecated Session id read from WSSession
*/
@Deprecated(since = "1.2.26")
public String getHttpSessionId() {
return wsSessionId;
}
/**
* Returns the user agent.
*
* @return userAgent
*/
public String getUserAgent() {
return userAgent;
}
/**
* Sets the incoming headers.
*
* @param headers
*/
public void setHeaders(Map> headers) {
if (headers != null && !headers.isEmpty()) {
// look for both upper and lower case
List userAgentHeader = Optional.ofNullable(headers.get(WSConstants.HTTP_HEADER_USERAGENT)).orElse(headers.get(WSConstants.HTTP_HEADER_USERAGENT.toLowerCase()));
if (userAgentHeader != null && !userAgentHeader.isEmpty()) {
userAgent = userAgentHeader.get(0);
}
List hostHeader = Optional.ofNullable(headers.get(Constants.HOST_HEADER_NAME)).orElse(headers.get(Constants.HOST_HEADER_NAME.toLowerCase()));
if (hostHeader != null && !hostHeader.isEmpty()) {
host = hostHeader.get(0);
}
List originHeader = Optional.ofNullable(headers.get(Constants.ORIGIN_HEADER_NAME)).orElse(headers.get(Constants.ORIGIN_HEADER_NAME.toLowerCase()));
if (originHeader != null && !originHeader.isEmpty()) {
origin = originHeader.get(0);
}
Optional> protocolHeader = Optional.ofNullable(headers.get(WSConstants.WS_HEADER_PROTOCOL));
if (protocolHeader.isPresent()) {
if (isDebug) {
log.debug("Protocol header(s) exist: {}", protocolHeader.get());
}
protocol = protocolHeader.get().get(0);
}
if (isDebug) {
log.debug("Set from headers - user-agent: {} host: {} origin: {}", userAgent, host, origin);
}
this.headers = headers;
} else {
this.headers = Collections.emptyMap();
}
}
public Map> getHeaders() {
return headers;
}
public Map getQuerystringParameters() {
return querystringParameters;
}
public void setQuerystringParameters(Map querystringParameters) {
if (this.querystringParameters == null) {
this.querystringParameters = new ConcurrentHashMap<>();
}
this.querystringParameters.putAll(querystringParameters);
}
/**
* Returns whether or not extensions are enabled on this connection.
*
* @return true if extensions are enabled, false otherwise
*/
public boolean hasExtensions() {
return extensions != null && !extensions.isEmpty();
}
/**
* Returns enabled extensions.
*
* @return extensions
*/
public Map getExtensions() {
return extensions;
}
/**
* Sets the extensions.
*
* @param extensions
*/
public void setExtensions(Map extensions) {
this.extensions = extensions;
}
/**
* Returns the extensions list as a comma separated string as specified by the rfc.
*
* @return extension list string or null if no extensions are enabled
*/
public String getExtensionsAsString() {
String extensionsList = null;
if (extensions != null) {
StringBuilder sb = new StringBuilder();
for (String key : extensions.keySet()) {
sb.append(key);
sb.append("; ");
}
extensionsList = sb.toString().trim();
}
return extensionsList;
}
/**
* Returns whether or not a protocol is enabled on this connection.
*
* @return true if protocol is enabled, false otherwise
*/
public boolean hasProtocol() {
return protocol != null;
}
/**
* Returns the protocol enabled on this connection.
*
* @return protocol
*/
public String getProtocol() {
return protocol;
}
/**
* Sets the protocol.
*
* @param protocol
*/
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public static long getSendTimeout() {
return sendTimeout;
}
public static void setSendTimeout(long sendTimeout) {
WebSocketConnection.sendTimeout = sendTimeout;
}
public static long getReadTimeout() {
return readTimeout;
}
public static void setReadTimeout(long readTimeout) {
WebSocketConnection.readTimeout = readTimeout;
}
public void setUserProperty(String key, Object value) {
WsSession wsSession = getWsSession();
if (wsSession != null) {
wsSession.getUserProperties().put(key, value);
}
}
public Object getUserProperty(String key) {
WsSession wsSession = getWsSession();
if (wsSession.getUserProperties().get(key) != null) {
return wsSession.getUserProperties().get(key);
}
return null;
}
public void setWsSessionTimeout(long idleTimeout) {
if (wsSession != null) {
wsSession.setMaxIdleTimeout(idleTimeout);
}
}
public WsSession getWsSession() {
return wsSession != null ? wsSession : null;
}
public long getReadBytes() {
return readBytes;
}
public void updateReadBytes(long read) {
log.debug("updateReadBytes: {} by: {}", readBytes, read);
readBytesUpdater.addAndGet(this, read);
// read time is updated on WsSession by WsFrameBase when the read is performed
}
public long getWrittenBytes() {
return writtenBytes;
}
public void updateWriteBytes(long wrote) {
log.debug("updateWriteBytes: {} by: {}", writtenBytes, wrote);
writeBytesUpdater.addAndGet(this, wrote);
// write time is updated on WsSession by WsRemoteEndpointImplBase when the write is performed
}
public String getWsSessionId() {
return wsSessionId;
}
@Override
public int compareTo(WebSocketConnection that) {
return Integer.compare(hashCode, that.hashCode);
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
WebSocketConnection other = (WebSocketConnection) obj;
return hashCode == other.hashCode();
}
@Override
public String toString() {
if (wsSessionId != null) {
return "WebSocketConnection [wsId=" + wsSessionId + ", host=" + host + ", origin=" + origin + ", path=" + path + ", secure=" + isSecure() + ", connected=" + connected + "]";
}
if (wsSession == null) {
return "WebSocketConnection [wsId=not-set, host=" + host + ", origin=" + origin + ", path=" + path + ", secure=not-set, connected=" + connected + "]";
}
return "WebSocketConnection [host=" + host + ", origin=" + origin + ", path=" + path + " connected=false]";
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy