1 /*
2 * Copyright 2025 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package io.netty.handler.codec.http3;
17
18 import io.netty.util.collection.LongObjectHashMap;
19 import io.netty.util.collection.LongObjectMap;
20
21 import javax.annotation.Nullable;
22 import java.util.Iterator;
23 import java.util.Map;
24
25 import static java.lang.Long.toHexString;
26 import static io.netty.util.internal.ObjectUtil.checkNotNull;
27
28 /**
29 * Represents a collection of HTTP/3 settings as defined by the
30 * <a href="https://datatracker.ietf.org/doc/html/rfc9114#section-7.2.4">
31 * HTTP/3 specification</a>.
32 *
33 * <p>This class provides type-safe accessors for standard HTTP/3 settings such as:
34 * <ul>
35 * <li>{@code QPACK_MAX_TABLE_CAPACITY} (0x1)</li>
36 * <li>{@code MAX_FIELD_SECTION_SIZE} (0x6)</li>
37 * <li>{@code QPACK_BLOCKED_STREAMS} (0x7)</li>
38 * <li>{@code ENABLE_CONNECT_PROTOCOL} (0x8)</li>
39 * <li>{@code H3_DATAGRAM} (0x33)</>
40 * </ul>
41 *
42 * Non-standard settings are ignored
43 * Reserved HTTP/2 setting identifiers are rejected.
44 *
45 */
46 public final class Http3Settings implements Iterable<Map.Entry<Long, Long>> {
47
48 private final LongObjectMap<Long> settings;
49 final NonStandardHttp3SettingsValidator nonStandardSettingsValidator;
50 private static final Long TRUE = 1L;
51 private static final Long FALSE = 0L;
52
53 /**
54 * Creates a new instance
55 */
56 public Http3Settings() {
57 // Ignore non-standard settings by default.
58 this((id, v) -> false);
59 }
60
61 /**
62 * Creates a new instance
63 *
64 * @param nonStandardSettingsValidator the {@link NonStandardHttp3SettingsValidator} to use to check if a specific
65 * setting that is non-standard should be supported or not.
66 */
67 public Http3Settings(NonStandardHttp3SettingsValidator nonStandardSettingsValidator) {
68 this.settings = new LongObjectHashMap<>(Http3SettingIdentifier.values().length);
69 this.nonStandardSettingsValidator = checkNotNull(nonStandardSettingsValidator, "nonStandardSettingsValidator");
70 }
71
72 /**
73 * Stores a setting value for the specified identifier.
74 * <p>
75 * The key and value are validated according to the HTTP/3 specification.
76 * Reserved HTTP/2 setting identifiers and negative values are not allowed.
77 * Ignore any unknown id/key as per <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-9>rfc9114</a>
78 * @param key the numeric setting identifier
79 * @param value the setting value (non-null)
80 * @return the previous value associated with the key, or {@code null} if none
81 * @throws IllegalArgumentException if the key or value is invalid
82 */
83 @Nullable
84 public Long put(long key, Long value) {
85
86 // When HTTP2 settings identifier present - Throw Error
87 if (Http3CodecUtils.isReservedHttp2Setting(key)) {
88 throw new IllegalArgumentException("Setting is reserved for HTTP/2: " + key);
89 }
90
91 Http3SettingIdentifier identifier = Http3SettingIdentifier.fromId(key);
92
93 if (identifier == null) {
94 // When Non-Standard/Unknown settings identifier present check if we should ignore it or not.
95 if (!nonStandardSettingsValidator.validate(key, value)) {
96 return null;
97 }
98 } else {
99 //Validation
100 verifyStandardSetting(identifier, value);
101 }
102
103 return settings.put(key, value);
104 }
105
106 /**
107 * Returns the value of the specified setting identifier.
108 *
109 * @param key the numeric setting identifier
110 * @return the setting value, or {@code null} if not set
111 */
112 @Nullable
113 public Long get(long key) {
114 return settings.get(key);
115 }
116
117 /**
118 * Returns the value associated with the specified setting identifier,
119 * or {@code defaultValue} if no value is present for the given key.
120 *
121 * @param key the numeric setting identifier
122 * @param defaultValue the value to return if the setting is not present
123 * @return the configured value for the specified key, or {@code defaultValue},
124 * if the key is not found
125 */
126 public long getOrDefault(long key, long defaultValue) {
127 return settings.getOrDefault(key, defaultValue);
128 }
129
130 /**
131 * Returns the {@code QPACK_MAX_TABLE_CAPACITY} value.
132 *
133 * @return the current QPACK maximum table capacity, or {@code null} if not set
134 */
135 @Nullable
136 public Long qpackMaxTableCapacity() {
137 return get(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY.id());
138 }
139
140 /**
141 * Sets the {@code QPACK_MAX_TABLE_CAPACITY} value.
142 *
143 * @param value QPACK maximum table capacity (must be ≥ 0)
144 * @return this instance for method chaining
145 */
146 public Http3Settings qpackMaxTableCapacity(long value) {
147 put(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY.id(), value);
148 return this;
149 }
150
151 /**
152 * Returns the {@code MAX_FIELD_SECTION_SIZE} value.
153 *
154 * @return the maximum field section size, or {@code null} if not set
155 */
156 @Nullable
157 public Long maxFieldSectionSize() {
158 return get(Http3SettingIdentifier.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE.id());
159 }
160
161 /**
162 * Sets the {@code MAX_FIELD_SECTION_SIZE} value.
163 *
164 * @param value maximum field section size (must be ≥ 0)
165 * @return this instance for method chaining
166 */
167 public Http3Settings maxFieldSectionSize(long value) {
168 put(Http3SettingIdentifier.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE.id(), value);
169 return this;
170 }
171
172 /**
173 * Returns the {@code QPACK_BLOCKED_STREAMS} value.
174 *
175 * @return the number of blocked streams, or {@code null} if not set
176 */
177 @Nullable
178 public Long qpackBlockedStreams() {
179 return get(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS.id());
180 }
181
182 /**
183 * Sets the {@code QPACK_BLOCKED_STREAMS} value.
184 *
185 * @param value number of blocked streams (must be ≥ 0)
186 * @return this instance for method chaining
187 */
188 public Http3Settings qpackBlockedStreams(long value) {
189 put(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS.id(), value);
190 return this;
191 }
192
193 /**
194 * Returns whether the {@code ENABLE_CONNECT_PROTOCOL} setting is enabled.
195 *
196 * @return {@code true} if enabled, {@code false} if disabled, or {@code null} if not set
197 */
198 @Nullable
199 public Boolean connectProtocolEnabled() {
200 Long value = get(Http3SettingIdentifier.HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL.id());
201 return value == null ? null : TRUE.equals(value);
202 }
203
204 /**
205 * Sets the {@code ENABLE_CONNECT_PROTOCOL} flag.
206 *
207 * @param enabled whether to enable the CONNECT protocol
208 * @return this instance for method chaining
209 */
210 public Http3Settings enableConnectProtocol(boolean enabled) {
211 put(Http3SettingIdentifier.HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL.id(), enabled ? TRUE : FALSE);
212 return this;
213 }
214
215 /**
216 * Returns whether the {@code H3_DATAGRAM} setting is enabled.
217 *
218 * @return {@code true} if enabled, {@code false} if disabled, or {@code null} if not set
219 */
220 @Nullable
221 public Boolean h3DatagramEnabled() {
222 Long value = get(Http3SettingIdentifier.HTTP3_SETTINGS_H3_DATAGRAM.id());
223 return value == null ? null : TRUE.equals(value);
224 }
225
226 /**
227 * Sets the {@code H3_DATAGRAM} settings identifier.
228 *
229 * @param enabled whether to enable the H3 Datagram
230 * @return this instance for method chaining
231 */
232 public Http3Settings enableH3Datagram(boolean enabled) {
233 put(Http3SettingIdentifier.HTTP3_SETTINGS_H3_DATAGRAM.id(), enabled ? TRUE : FALSE);
234 return this;
235 }
236
237 /**
238 * Replaces all current settings with those from another {@link Http3Settings} instance.
239 *
240 * @param http3Settings the source settings (non-null)
241 * @return this instance for method chaining
242 */
243 public Http3Settings putAll(Http3Settings http3Settings) {
244 checkNotNull(http3Settings, "http3Settings");
245 settings.putAll(http3Settings.settings);
246 return this;
247 }
248
249 /**
250 * Returns a new {@link Http3Settings} instance with default values:
251 * <ul>
252 * <li>{@code QPACK_MAX_TABLE_CAPACITY} = 0</li>
253 * <li>{@code QPACK_BLOCKED_STREAMS} = 0</li>
254 * <li>{@code ENABLE_CONNECT_PROTOCOL} = false</li>
255 * <li>{@code MAX_FIELD_SECTION_SIZE} = 8192</li>
256 * <li>{@code H3_DATAGRAM} = false </>
257 * </ul>
258 *
259 * @return a default {@link Http3Settings} instance
260 */
261 public static Http3Settings defaultSettings() {
262 return new Http3Settings()
263 .qpackMaxTableCapacity(0)
264 .qpackBlockedStreams(0)
265 .maxFieldSectionSize(Http3CodecUtils.DEFAULT_MAX_FIELD_SECTION_SIZE)
266 .enableConnectProtocol(false)
267 .enableH3Datagram(false);
268 }
269
270 /**
271 * Returns an iterator over the settings entries in this object.
272 * Each entry’s key is the numeric setting identifier, and the value is its numeric value.
273 *
274 * @return an iterator over immutable {@link Map.Entry} objects
275 */
276 @Override
277 public Iterator<Map.Entry<Long, Long>> iterator() {
278 Iterator<LongObjectMap.PrimitiveEntry<Long>> it = settings.entries().iterator();
279 return new Iterator<Map.Entry<Long, Long>>() {
280 @Override
281 public boolean hasNext() {
282 return it.hasNext();
283 }
284
285 @Override
286 public Map.Entry<Long, Long> next() {
287 LongObjectMap.PrimitiveEntry<Long> entry = it.next();
288 return new java.util.AbstractMap.SimpleImmutableEntry<>(entry.key(), entry.value());
289 }
290 };
291 }
292
293 /**
294 * Compares this settings object to another for equality.
295 * Two instances are equal if they contain the same key–value pairs.
296 *
297 * @param o the other object
298 * @return {@code true} if equal, {@code false} otherwise
299 */
300 @Override
301 public boolean equals(Object o) {
302 if (this == o) {
303 return true;
304 }
305 if (!(o instanceof Http3Settings)) {
306 return false;
307 }
308 Http3Settings that = (Http3Settings) o;
309 return settings.equals(that.settings);
310 }
311
312 /**
313 * Returns the hash code of this settings object, based on its key–value pairs.
314 *
315 * @return the hash code
316 */
317 @Override
318 public int hashCode() {
319 return settings.hashCode();
320 }
321
322 /**
323 * Returns a string representation of this settings object in the form:
324 * <pre>
325 * Http3Settings{0x1=100, 0x6=16384, 0x7=0}
326 * </pre>
327 *
328 * @return a human-readable string representation of the settings
329 */
330 @Override
331 public String toString() {
332 StringBuilder sb = new StringBuilder("Http3Settings{");
333 boolean first = true;
334 for (LongObjectMap.PrimitiveEntry<Long> e : settings.entries()) {
335 if (!first) {
336 sb.append(", ");
337 }
338 first = false;
339 sb.append("0x").append(toHexString(e.key())).append('=').append(e.value());
340 }
341 return sb.append('}').toString();
342 }
343
344 /**
345 * Validates a setting identifier and value pair against HTTP/3.
346 * Note that it can only validate the valid HTTP/3 settings
347 * Does not validate non-standard settings
348 * @param identifier the setting identifier
349 * @param value the setting value
350 * @throws IllegalArgumentException if the identifier or value violates the protocol specification
351 */
352 private static void verifyStandardSetting(Http3SettingIdentifier identifier, Long value) {
353 checkNotNull(value, "value");
354 checkNotNull(identifier, "identifier");
355
356 switch (identifier) {
357 case HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY:
358 case HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS:
359 case HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE:
360 if (value < 0) {
361 throw new IllegalArgumentException("Setting 0x" + toHexString(identifier.id())
362 + " invalid: " + value + " (must be >= 0)");
363 }
364 break;
365 case HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL:
366 case HTTP3_SETTINGS_H3_DATAGRAM:
367 if (value != 0L && value != 1L) {
368 throw new IllegalArgumentException(
369 "Invalid: " + value + "for "
370 + Http3SettingIdentifier.valueOf(String.valueOf(identifier))
371 + " (expected 0 or 1)");
372 }
373 break;
374 default:
375 if (value < 0) {
376 throw new IllegalArgumentException("Setting 0x"
377 + toHexString(identifier.id()) + " invalid: " + value);
378 }
379 }
380 }
381
382 /**
383 * Allows to handle non-standard settings. By default non-standard settings will be ignore as defined by the
384 * RFC.
385 */
386 public interface NonStandardHttp3SettingsValidator {
387 /**
388 * Validate the setting with the given id and value.
389 *
390 * @param id the id of the setting
391 * @param value the value of the setting
392 * @return {@code true} if the settings is supported, {@code false} otherwise.
393 * @throws IllegalArgumentException if the given {@code value} is not supported for the id.
394 */
395 boolean validate(long id, Long value) throws IllegalArgumentException;
396 }
397 }