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 {@code QPACK_MAX_TABLE_CAPACITY} value.
119 *
120 * @return the current QPACK maximum table capacity, or {@code null} if not set
121 */
122 @Nullable
123 public Long qpackMaxTableCapacity() {
124 return get(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY.id());
125 }
126
127 /**
128 * Sets the {@code QPACK_MAX_TABLE_CAPACITY} value.
129 *
130 * @param value QPACK maximum table capacity (must be ≥ 0)
131 * @return this instance for method chaining
132 */
133 public Http3Settings qpackMaxTableCapacity(long value) {
134 put(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY.id(), value);
135 return this;
136 }
137
138 /**
139 * Returns the {@code MAX_FIELD_SECTION_SIZE} value.
140 *
141 * @return the maximum field section size, or {@code null} if not set
142 */
143 @Nullable
144 public Long maxFieldSectionSize() {
145 return get(Http3SettingIdentifier.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE.id());
146 }
147
148 /**
149 * Sets the {@code MAX_FIELD_SECTION_SIZE} value.
150 *
151 * @param value maximum field section size (must be ≥ 0)
152 * @return this instance for method chaining
153 */
154 public Http3Settings maxFieldSectionSize(long value) {
155 put(Http3SettingIdentifier.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE.id(), value);
156 return this;
157 }
158
159 /**
160 * Returns the {@code QPACK_BLOCKED_STREAMS} value.
161 *
162 * @return the number of blocked streams, or {@code null} if not set
163 */
164 @Nullable
165 public Long qpackBlockedStreams() {
166 return get(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS.id());
167 }
168
169 /**
170 * Sets the {@code QPACK_BLOCKED_STREAMS} value.
171 *
172 * @param value number of blocked streams (must be ≥ 0)
173 * @return this instance for method chaining
174 */
175 public Http3Settings qpackBlockedStreams(long value) {
176 put(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS.id(), value);
177 return this;
178 }
179
180 /**
181 * Returns whether the {@code ENABLE_CONNECT_PROTOCOL} setting is enabled.
182 *
183 * @return {@code true} if enabled, {@code false} if disabled, or {@code null} if not set
184 */
185 @Nullable
186 public Boolean connectProtocolEnabled() {
187 Long value = get(Http3SettingIdentifier.HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL.id());
188 return value == null ? null : TRUE.equals(value);
189 }
190
191 /**
192 * Sets the {@code ENABLE_CONNECT_PROTOCOL} flag.
193 *
194 * @param enabled whether to enable the CONNECT protocol
195 * @return this instance for method chaining
196 */
197 public Http3Settings enableConnectProtocol(boolean enabled) {
198 put(Http3SettingIdentifier.HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL.id(), enabled ? TRUE : FALSE);
199 return this;
200 }
201
202 /**
203 * Returns whether the {@code H3_DATAGRAM} setting is enabled.
204 *
205 * @return {@code true} if enabled, {@code false} if disabled, or {@code null} if not set
206 */
207 @Nullable
208 public Boolean h3DatagramEnabled() {
209 Long value = get(Http3SettingIdentifier.HTTP3_SETTINGS_H3_DATAGRAM.id());
210 return value == null ? null : TRUE.equals(value);
211 }
212
213 /**
214 * Sets the {@code H3_DATAGRAM} settings identifier.
215 *
216 * @param enabled whether to enable the H3 Datagram
217 * @return this instance for method chaining
218 */
219 public Http3Settings enableH3Datagram(boolean enabled) {
220 put(Http3SettingIdentifier.HTTP3_SETTINGS_H3_DATAGRAM.id(), enabled ? TRUE : FALSE);
221 return this;
222 }
223
224 /**
225 * Replaces all current settings with those from another {@link Http3Settings} instance.
226 *
227 * @param http3Settings the source settings (non-null)
228 * @return this instance for method chaining
229 */
230 public Http3Settings putAll(Http3Settings http3Settings) {
231 checkNotNull(http3Settings, "http3Settings");
232 settings.putAll(http3Settings.settings);
233 return this;
234 }
235
236 /**
237 * Returns a new {@link Http3Settings} instance with default values:
238 * <ul>
239 * <li>{@code QPACK_MAX_TABLE_CAPACITY} = 0</li>
240 * <li>{@code QPACK_BLOCKED_STREAMS} = 0</li>
241 * <li>{@code ENABLE_CONNECT_PROTOCOL} = false</li>
242 * <li>{@code MAX_FIELD_SECTION_SIZE} = unlimited</li>
243 * <li>{@code H3_DATAGRAM} = false </>
244 * </ul>
245 *
246 * @return a default {@link Http3Settings} instance
247 */
248 public static Http3Settings defaultSettings() {
249 return new Http3Settings()
250 .qpackMaxTableCapacity(0)
251 .qpackBlockedStreams(0)
252 .maxFieldSectionSize(16 * 1024 * 1024)
253 .enableConnectProtocol(false)
254 .enableH3Datagram(false);
255 }
256
257 /**
258 * Returns an iterator over the settings entries in this object.
259 * Each entry’s key is the numeric setting identifier, and the value is its numeric value.
260 *
261 * @return an iterator over immutable {@link Map.Entry} objects
262 */
263 @Override
264 public Iterator<Map.Entry<Long, Long>> iterator() {
265 Iterator<LongObjectMap.PrimitiveEntry<Long>> it = settings.entries().iterator();
266 return new Iterator<Map.Entry<Long, Long>>() {
267 @Override
268 public boolean hasNext() {
269 return it.hasNext();
270 }
271
272 @Override
273 public Map.Entry<Long, Long> next() {
274 LongObjectMap.PrimitiveEntry<Long> entry = it.next();
275 return new java.util.AbstractMap.SimpleImmutableEntry<>(entry.key(), entry.value());
276 }
277 };
278 }
279
280 /**
281 * Compares this settings object to another for equality.
282 * Two instances are equal if they contain the same key–value pairs.
283 *
284 * @param o the other object
285 * @return {@code true} if equal, {@code false} otherwise
286 */
287 @Override
288 public boolean equals(Object o) {
289 if (this == o) {
290 return true;
291 }
292 if (!(o instanceof Http3Settings)) {
293 return false;
294 }
295 Http3Settings that = (Http3Settings) o;
296 return settings.equals(that.settings);
297 }
298
299 /**
300 * Returns the hash code of this settings object, based on its key–value pairs.
301 *
302 * @return the hash code
303 */
304 @Override
305 public int hashCode() {
306 return settings.hashCode();
307 }
308
309 /**
310 * Returns a string representation of this settings object in the form:
311 * <pre>
312 * Http3Settings{0x1=100, 0x6=16384, 0x7=0}
313 * </pre>
314 *
315 * @return a human-readable string representation of the settings
316 */
317 @Override
318 public String toString() {
319 StringBuilder sb = new StringBuilder("Http3Settings{");
320 boolean first = true;
321 for (LongObjectMap.PrimitiveEntry<Long> e : settings.entries()) {
322 if (!first) {
323 sb.append(", ");
324 }
325 first = false;
326 sb.append("0x").append(toHexString(e.key())).append('=').append(e.value());
327 }
328 return sb.append('}').toString();
329 }
330
331 /**
332 * Validates a setting identifier and value pair against HTTP/3.
333 * Note that it can only validate the valid HTTP/3 settings
334 * Does not validate non-standard settings
335 * @param identifier the setting identifier
336 * @param value the setting value
337 * @throws IllegalArgumentException if the identifier or value violates the protocol specification
338 */
339 private static void verifyStandardSetting(Http3SettingIdentifier identifier, Long value) {
340 checkNotNull(value, "value");
341 checkNotNull(identifier, "identifier");
342
343 switch (identifier) {
344 case HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY:
345 case HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS:
346 case HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE:
347 if (value < 0) {
348 throw new IllegalArgumentException("Setting 0x" + toHexString(identifier.id())
349 + " invalid: " + value + " (must be >= 0)");
350 }
351 break;
352 case HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL:
353 case HTTP3_SETTINGS_H3_DATAGRAM:
354 if (value != 0L && value != 1L) {
355 throw new IllegalArgumentException(
356 "Invalid: " + value + "for "
357 + Http3SettingIdentifier.valueOf(String.valueOf(identifier))
358 + " (expected 0 or 1)");
359 }
360 break;
361 default:
362 if (value < 0) {
363 throw new IllegalArgumentException("Setting 0x"
364 + toHexString(identifier.id()) + " invalid: " + value);
365 }
366 }
367 }
368
369 /**
370 * Allows to handle non-standard settings. By default non-standard settings will be ignore as defined by the
371 * RFC.
372 */
373 public interface NonStandardHttp3SettingsValidator {
374 /**
375 * Validate the setting with the given id and value.
376 *
377 * @param id the id of the setting
378 * @param value the value of the setting
379 * @return {@code true} if the settings is supported, {@code false} otherwise.
380 * @throws IllegalArgumentException if the given {@code value} is not supported for the id.
381 */
382 boolean validate(long id, Long value) throws IllegalArgumentException;
383 }
384 }