297 lines
10 KiB
Java
297 lines
10 KiB
Java
package dev.coph.nextusweb.server.websocket;
|
|
|
|
import java.time.Duration;
|
|
import java.util.Collections;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* Immutable configuration for the WebSocket subsystem: frame and message size limits, idle
|
|
* timeout, allowed origins, negotiated subprotocols, and compression. Instances are created
|
|
* through the nested {@link Builder}.
|
|
*
|
|
* <p>The values configured here govern how {@code HttpRequestHandler} sets up the WebSocket
|
|
* portion of the Netty pipeline during the upgrade handshake.</p>
|
|
*/
|
|
public final class WebSocketConfig {
|
|
|
|
/** Maximum size, in bytes, of a single WebSocket frame payload. */
|
|
private final int maxFramePayloadLength;
|
|
/** Maximum size, in bytes, of an aggregated (multi-frame) message. */
|
|
private final int maxAggregatedMessageSize;
|
|
/** Idle timeout after which an inactive connection is closed; {@code null} disables it. */
|
|
private final Duration idleTimeout;
|
|
/** Explicit set of allowed origins; ignored when {@link #allowAnyOrigin} is {@code true}. */
|
|
private final Set<String> allowedOrigins;
|
|
/** Whether connections from any origin are accepted. */
|
|
private final boolean allowAnyOrigin;
|
|
/** Subprotocols offered during negotiation. */
|
|
private final Set<String> subprotocols;
|
|
/** Whether per-message deflate compression is enabled. */
|
|
private final boolean compression;
|
|
/** Whether the protocol handler matches the path by prefix rather than exact equality. */
|
|
private final boolean checkStartsWith;
|
|
|
|
/**
|
|
* Builds an immutable configuration from a {@link Builder}, defensively copying its sets.
|
|
*
|
|
* @param b the builder carrying the configured values
|
|
*/
|
|
private WebSocketConfig(Builder b) {
|
|
this.maxFramePayloadLength = b.maxFramePayloadLength;
|
|
this.maxAggregatedMessageSize = b.maxAggregatedMessageSize;
|
|
this.idleTimeout = b.idleTimeout;
|
|
this.allowedOrigins = Set.copyOf(b.allowedOrigins);
|
|
this.allowAnyOrigin = b.allowAnyOrigin;
|
|
this.subprotocols = Set.copyOf(b.subprotocols);
|
|
this.compression = b.compression;
|
|
this.checkStartsWith = b.checkStartsWith;
|
|
}
|
|
|
|
/**
|
|
* Creates a configuration with all default values.
|
|
*
|
|
* @return a default configuration
|
|
*/
|
|
public static WebSocketConfig defaults() {
|
|
return builder().build();
|
|
}
|
|
|
|
/**
|
|
* Creates a new, empty {@link Builder}.
|
|
*
|
|
* @return a fresh builder
|
|
*/
|
|
public static Builder builder() {
|
|
return new Builder();
|
|
}
|
|
|
|
/**
|
|
* Tests whether a WebSocket upgrade from the given origin is permitted.
|
|
*
|
|
* @param origin the request's {@code Origin} header, may be {@code null}
|
|
* @return {@code true} if any origin is allowed, or if the origin is in the allow-list;
|
|
* {@code false} for a {@code null} or disallowed origin
|
|
*/
|
|
public boolean isOriginAllowed(String origin) {
|
|
if (allowAnyOrigin) return true;
|
|
if (origin == null) return false;
|
|
return allowedOrigins.contains(origin);
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum size of a single WebSocket frame payload.
|
|
*
|
|
* @return the maximum single-frame payload size in bytes
|
|
*/
|
|
public int maxFramePayloadLength() {
|
|
return maxFramePayloadLength;
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum size of an aggregated (multi-frame) message.
|
|
*
|
|
* @return the maximum aggregated message size in bytes
|
|
*/
|
|
public int maxAggregatedMessageSize() {
|
|
return maxAggregatedMessageSize;
|
|
}
|
|
|
|
/**
|
|
* Returns the idle timeout after which inactive connections are closed.
|
|
*
|
|
* @return the idle timeout, or {@code null} if idle connections are never closed
|
|
*/
|
|
public Duration idleTimeout() {
|
|
return idleTimeout;
|
|
}
|
|
|
|
/**
|
|
* Indicates whether connections from any origin are accepted.
|
|
*
|
|
* @return {@code true} if connections from any origin are accepted
|
|
*/
|
|
public boolean allowAnyOrigin() {
|
|
return allowAnyOrigin;
|
|
}
|
|
|
|
/**
|
|
* Returns the explicitly allowed origins.
|
|
*
|
|
* @return the immutable set of explicitly allowed origins
|
|
*/
|
|
public Set<String> allowedOrigins() {
|
|
return allowedOrigins;
|
|
}
|
|
|
|
/**
|
|
* Returns the configured subprotocols as a comma-separated string suitable for Netty's
|
|
* protocol config.
|
|
*
|
|
* @return the comma-separated subprotocol list, or {@code null} if none are configured
|
|
*/
|
|
public String subprotocolsCsv() {
|
|
if (subprotocols.isEmpty()) return null;
|
|
return String.join(",", subprotocols);
|
|
}
|
|
|
|
/**
|
|
* Indicates whether per-message deflate compression is enabled.
|
|
*
|
|
* @return {@code true} if per-message compression is enabled
|
|
*/
|
|
public boolean compression() {
|
|
return compression;
|
|
}
|
|
|
|
/**
|
|
* Indicates whether the WebSocket path is matched by prefix rather than exact equality.
|
|
*
|
|
* @return {@code true} if the WebSocket path is matched by prefix rather than exactly
|
|
*/
|
|
public boolean checkStartsWith() {
|
|
return checkStartsWith;
|
|
}
|
|
|
|
/**
|
|
* Fluent builder for {@link WebSocketConfig}, pre-populated with sensible defaults: 64 KiB
|
|
* frames, 1 MiB aggregated messages, a 60-second idle timeout, no origin restriction
|
|
* list, compression enabled, and exact path matching.
|
|
*/
|
|
public static final class Builder {
|
|
/** Maximum single-frame payload size in bytes; defaults to 64 KiB. */
|
|
private int maxFramePayloadLength = 65_536;
|
|
/** Maximum aggregated message size in bytes; defaults to 1 MiB. */
|
|
private int maxAggregatedMessageSize = 1_048_576;
|
|
/** Idle timeout; defaults to 60 seconds. */
|
|
private Duration idleTimeout = Duration.ofSeconds(60);
|
|
/** Accumulated allowed origins (insertion-ordered). */
|
|
private final Set<String> allowedOrigins = new LinkedHashSet<>();
|
|
/** Whether any origin is allowed; defaults to {@code false}. */
|
|
private boolean allowAnyOrigin = false;
|
|
/** Accumulated subprotocols (insertion-ordered). */
|
|
private final Set<String> subprotocols = new LinkedHashSet<>();
|
|
/** Whether compression is enabled; defaults to {@code true}. */
|
|
private boolean compression = true;
|
|
/** Whether path matching uses a prefix check; defaults to {@code false}. */
|
|
private boolean checkStartsWith = false;
|
|
|
|
/**
|
|
* Creates a builder pre-populated with the default configuration values described
|
|
* above. Obtain instances via {@link WebSocketConfig#builder()}.
|
|
*/
|
|
public Builder() {
|
|
}
|
|
|
|
/**
|
|
* Sets the maximum single-frame payload size.
|
|
*
|
|
* @param bytes the limit in bytes; must be positive
|
|
* @return this builder, for fluent chaining
|
|
* @throws IllegalArgumentException if {@code bytes <= 0}
|
|
*/
|
|
public Builder maxFramePayloadLength(int bytes) {
|
|
if (bytes <= 0) throw new IllegalArgumentException("maxFramePayloadLength must be > 0");
|
|
this.maxFramePayloadLength = bytes;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the maximum aggregated message size.
|
|
*
|
|
* @param bytes the limit in bytes; must be positive
|
|
* @return this builder, for fluent chaining
|
|
* @throws IllegalArgumentException if {@code bytes <= 0}
|
|
*/
|
|
public Builder maxAggregatedMessageSize(int bytes) {
|
|
if (bytes <= 0) throw new IllegalArgumentException("maxAggregatedMessageSize must be > 0");
|
|
this.maxAggregatedMessageSize = bytes;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the idle timeout after which inactive connections are closed.
|
|
*
|
|
* @param timeout the idle timeout
|
|
* @return this builder, for fluent chaining
|
|
*/
|
|
public Builder idleTimeout(Duration timeout) {
|
|
this.idleTimeout = timeout;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Disables the idle timeout, so connections are never closed for inactivity.
|
|
*
|
|
* @return this builder, for fluent chaining
|
|
*/
|
|
public Builder noIdleTimeout() {
|
|
this.idleTimeout = null;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Adds one or more origins to the allow-list.
|
|
*
|
|
* @param origins the origins to allow
|
|
* @return this builder, for fluent chaining
|
|
*/
|
|
public Builder allowedOrigins(String... origins) {
|
|
Collections.addAll(this.allowedOrigins, origins);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Allows WebSocket connections from any origin.
|
|
*
|
|
* @return this builder, for fluent chaining
|
|
*/
|
|
public Builder anyOrigin() {
|
|
this.allowAnyOrigin = true;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Adds one or more subprotocols to offer during negotiation.
|
|
*
|
|
* @param protocols the subprotocol names
|
|
* @return this builder, for fluent chaining
|
|
*/
|
|
public Builder subprotocols(String... protocols) {
|
|
Collections.addAll(this.subprotocols, protocols);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Enables or disables per-message compression.
|
|
*
|
|
* @param enabled {@code true} to enable compression
|
|
* @return this builder, for fluent chaining
|
|
*/
|
|
public Builder compression(boolean enabled) {
|
|
this.compression = enabled;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the WebSocket path is matched by prefix rather than exact equality.
|
|
*
|
|
* @param v {@code true} to match by prefix
|
|
* @return this builder, for fluent chaining
|
|
*/
|
|
public Builder checkStartsWith(boolean v) {
|
|
this.checkStartsWith = v;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Builds the immutable {@link WebSocketConfig}.
|
|
*
|
|
* @return the configured instance
|
|
*/
|
|
public WebSocketConfig build() {
|
|
return new WebSocketConfig(this);
|
|
}
|
|
}
|
|
}
|