Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP/2 set initial settings #2341

Merged
merged 4 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright © 2022 Apple Inc. and the ServiceTalk project authors
*
* 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 io.servicetalk.http.api;

import java.util.function.BiConsumer;
import javax.annotation.Nullable;

/**
* Object representing a <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.1">HTTP/2 Setting</a> frame.
*/
public interface Http2Settings {
/**
* Get the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_HEADER_TABLE_SIZE</a>.
* @return the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_HEADER_TABLE_SIZE</a>.
*/
@Nullable
Integer headerTableSize();

/**
* Get the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_CONCURRENT_STREAMS</a>.
* @return the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_CONCURRENT_STREAMS</a>.
*/
@Nullable
Integer maxConcurrentStreams();

/**
* Get the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_INITIAL_WINDOW_SIZE</a>.
* @return the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_INITIAL_WINDOW_SIZE</a>.
*/
@Nullable
Integer initialWindowSize();

/**
* Get the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_FRAME_SIZE</a>.
* @return the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_FRAME_SIZE</a>.
*/
@Nullable
Integer maxFrameSize();

/**
* Get the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a>.
* @return the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a>.
*/
@Nullable
Integer maxHeaderListSize();

/**
* Get the setting value associated with an
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.1">identifier</a>.
* @param identifier the <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.1">identifier</a>.
* @return {@code null} if no setting value corresponding {@code identifier} exists, otherwise the value.
*/
@Nullable
Integer settingValue(char identifier);
idelpivnitskiy marked this conversation as resolved.
Show resolved Hide resolved

/**
* Iterate over all the &lt;identifier, value&gt; tuple in this settings object.
* @param action Invoked on each &lt;identifier, value&gt; tuple.
*/
void forEach(BiConsumer<? super Character, ? super Integer> action);
idelpivnitskiy marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* Copyright © 2022 Apple Inc. and the ServiceTalk project authors
*
* 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 io.servicetalk.http.api;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;

/**
* Builder to help create a {@link Map} for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.1">HTTP/2 Setting</a>.
*/
public final class Http2SettingsBuilder {
/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_HEADER_TABLE_SIZE</a>.
*/
private static final char HEADER_TABLE_SIZE = 0x1;
/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">
* SETTINGS_MAX_CONCURRENT_STREAMS</a>.
*/
private static final char MAX_CONCURRENT_STREAMS = 0x3;
/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">
* SETTINGS_INITIAL_WINDOW_SIZE</a>.
*/
private static final char INITIAL_WINDOW_SIZE = 0x4;
/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_FRAME_SIZE</a>.
*/
private static final char MAX_FRAME_SIZE = 0x5;
/**
* Identifier <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">
* SETTINGS_MAX_HEADER_LIST_SIZE</a>.
*/
private static final char MAX_HEADER_LIST_SIZE = 0x6;
private final Map<Character, Integer> settings;

/**
* Create a new instance.
*/
public Http2SettingsBuilder() {
this(8);
}

/**
* Create a new instance.
* @param initialSize The initial size of the map.
*/
Http2SettingsBuilder(int initialSize) {
Scottmitch marked this conversation as resolved.
Show resolved Hide resolved
settings = new HashMap<>(initialSize);
}

/**
* Set the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_HEADER_TABLE_SIZE</a>.
*
* @param value The value.
* @return {@code this}.
*/
public Http2SettingsBuilder headerTableSize(int value) {
settings.put(HEADER_TABLE_SIZE, value);
return this;
}

/**
* Set the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_CONCURRENT_STREAMS</a>.
*
* @param value The value.
* @return {@code this}.
*/
public Http2SettingsBuilder maxConcurrentStreams(int value) {
settings.put(MAX_CONCURRENT_STREAMS, value);
Scottmitch marked this conversation as resolved.
Show resolved Hide resolved
return this;
}

/**
* Set the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_INITIAL_WINDOW_SIZE</a>.
*
* @param value The value.
* @return {@code this}.
*/
public Http2SettingsBuilder initialWindowSize(int value) {
settings.put(INITIAL_WINDOW_SIZE, value);
return this;
}

/**
* Set the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_FRAME_SIZE</a>.
*
* @param value The value.
* @return {@code this}.
*/
public Http2SettingsBuilder maxFrameSize(int value) {
settings.put(MAX_FRAME_SIZE, value);
return this;
}

/**
* Set the value for
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a>.
*
* @param value The value.
* @return {@code this}.
*/
public Http2SettingsBuilder maxHeaderListSize(int value) {
settings.put(MAX_HEADER_LIST_SIZE, value);
return this;
}

/**
* Build the {@link Map} that represents
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.1">HTTP/2 Setting</a>.
* @return the {@link Map} that represents
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.1">HTTP/2 Setting</a>.
*/
public Http2Settings build() {
return new DefaultHttp2Settings(settings);
}

private static final class DefaultHttp2Settings implements Http2Settings {
private final Map<Character, Integer> settings;

private DefaultHttp2Settings(final Map<Character, Integer> settings) {
this.settings = settings;
Scottmitch marked this conversation as resolved.
Show resolved Hide resolved
}

@Nullable
@Override
public Integer headerTableSize() {
return settings.get(HEADER_TABLE_SIZE);
}

@Nullable
@Override
public Integer maxConcurrentStreams() {
return settings.get(MAX_CONCURRENT_STREAMS);
}

@Nullable
@Override
public Integer initialWindowSize() {
return settings.get(INITIAL_WINDOW_SIZE);
}

@Nullable
@Override
public Integer maxFrameSize() {
return settings.get(MAX_FRAME_SIZE);
}

@Nullable
@Override
public Integer maxHeaderListSize() {
return settings.get(MAX_HEADER_LIST_SIZE);
}

@Nullable
@Override
public Integer settingValue(final char identifier) {
return settings.get(identifier);
}

@Override
public void forEach(final BiConsumer<? super Character, ? super Integer> action) {
settings.forEach(action);
}

@Override
public String toString() {
return settings.toString();
Scottmitch marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public int hashCode() {
return settings.hashCode();
}

@Override
public boolean equals(Object o) {
return o instanceof DefaultHttp2Settings && settings.equals(((DefaultHttp2Settings) o).settings);
Scottmitch marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
5 changes: 5 additions & 0 deletions servicetalk-http-netty/gradle/spotbugs/main-exclusions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,9 @@
<Method name="handlerAdded"/>
<Bug pattern="THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION"/>
</Match>
<Match>
<Class name="io.servicetalk.http.netty.H2ProtocolConfigBuilder"/>
<Field name="h2Settings"/>
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
</FindBugsFilter>
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,26 @@
import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame;
import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
import io.netty.handler.codec.http2.Http2MultiplexHandler;

import java.util.function.BiPredicate;
import io.netty.handler.codec.http2.Http2Settings;

import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.servicetalk.http.netty.H2ServerParentChannelInitializer.initFrameLogger;
import static io.servicetalk.http.netty.H2ServerParentChannelInitializer.toNettySettings;

final class H2ClientParentChannelInitializer implements ChannelInitializer {

private final H2ProtocolConfig config;
private final io.netty.handler.codec.http2.Http2Settings nettySettings;

H2ClientParentChannelInitializer(final H2ProtocolConfig config) {
this.config = config;
final io.servicetalk.http.api.Http2Settings h2Settings = config.initialSettings();
nettySettings = validateClientSettings(toNettySettings(h2Settings));
Scottmitch marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public void init(final Channel channel) {
final Http2FrameCodecBuilder multiplexCodecBuilder = new OptimizedHttp2FrameCodecBuilder(false)
final Http2FrameCodecBuilder multiplexCodecBuilder =
new OptimizedHttp2FrameCodecBuilder(false, config.flowControlQuantum())
// We do not want close to trigger graceful closure (go away), instead when user triggers a graceful
// close, we do the appropriate go away handling.
.decoupleCloseAndGoAway(true)
Expand All @@ -51,18 +54,13 @@ public void init(final Channel channel) {
.autoAckPingFrame(false)
// We don't want to rely upon Netty to manage the graceful close timeout, because we expect
// the user to apply their own timeout at the call site.
.gracefulShutdownTimeoutMillis(-1);

// Notify server that this client does not support server push and request it to be disabled.
multiplexCodecBuilder.initialSettings().pushEnabled(false).maxConcurrentStreams(0L);

final BiPredicate<CharSequence, CharSequence> headersSensitivityDetector =
config.headersSensitivityDetector();
multiplexCodecBuilder.headerSensitivityDetector(headersSensitivityDetector::test);
.gracefulShutdownTimeoutMillis(-1)
.initialSettings(nettySettings)
.headerSensitivityDetector(config.headersSensitivityDetector()::test);

initFrameLogger(multiplexCodecBuilder, config.frameLoggerConfig());

// TODO(scott): more configuration. header validation, settings stream, etc...
// TODO(scott): more configuration. header validation, etc...

channel.pipeline().addLast(multiplexCodecBuilder.build(),
new Http2MultiplexHandler(H2PushStreamHandler.INSTANCE));
Expand All @@ -84,4 +82,15 @@ public void channelRegistered(final ChannelHandlerContext ctx) {
// Http2ConnectionHandler.processGoAwayWriteResult will close the connection after GO_AWAY is flushed
}
}

private static Http2Settings validateClientSettings(Http2Settings settings) {
final Boolean pushEnabled = settings.pushEnabled();
if (pushEnabled == null) {
// Notify server that this client does not support server push and request it to be disabled.
settings.pushEnabled(false);
} else if (pushEnabled) {
throw new IllegalArgumentException("push is enabled but not supported. settings=" + settings);
Scottmitch marked this conversation as resolved.
Show resolved Hide resolved
}
return settings;
}
}
Loading