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}. * *

The values configured here govern how {@code HttpRequestHandler} sets up the WebSocket * portion of the Netty pipeline during the upgrade handshake.

*/ 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 allowedOrigins; /** Whether connections from any origin are accepted. */ private final boolean allowAnyOrigin; /** Subprotocols offered during negotiation. */ private final Set 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); } /** * @return the maximum single-frame payload size in bytes */ public int maxFramePayloadLength() { return maxFramePayloadLength; } /** * @return the maximum aggregated message size in bytes */ public int maxAggregatedMessageSize() { return maxAggregatedMessageSize; } /** * @return the idle timeout, or {@code null} if idle connections are never closed */ public Duration idleTimeout() { return idleTimeout; } /** * @return {@code true} if connections from any origin are accepted */ public boolean allowAnyOrigin() { return allowAnyOrigin; } /** * @return the immutable set of explicitly allowed origins */ public Set 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); } /** * @return {@code true} if per-message compression is enabled */ public boolean compression() { return compression; } /** * @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 allowedOrigins = new LinkedHashSet<>(); /** Whether any origin is allowed; defaults to {@code false}. */ private boolean allowAnyOrigin = false; /** Accumulated subprotocols (insertion-ordered). */ private final Set 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; /** * 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); } } }