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

Validate request size against global or endpoint provided max request size #28

Merged
merged 1 commit into from
Mar 13, 2017

Conversation

rabeyta
Copy link
Contributor

@rabeyta rabeyta commented Mar 10, 2017

issue #27

This PR will add the functionality of validating each request against a globally configured request size.

By default, endpoints do not override and will default to the global.

You can override the global configuration inside each endpoint.

ProxyRouterEndpoints have this validation disabled by default

@rabeyta rabeyta changed the title Validate request against global or endpoint provided max request size Validate request size against global or endpoint provided max request size Mar 10, 2017
@codecov-io
Copy link

codecov-io commented Mar 10, 2017

Codecov Report

Merging #28 into master will increase coverage by 0.01%.
The diff coverage is 95.45%.

@@             Coverage Diff             @@
##             master     #28      +/-   ##
===========================================
+ Coverage     89.79%   89.8%   +0.01%     
- Complexity     1823    1832       +9     
===========================================
  Files           137     137              
  Lines          5341    5357      +16     
  Branches        700     703       +3     
===========================================
+ Hits           4796    4811      +15     
  Misses          368     368              
- Partials        177     178       +1
Impacted Files Coverage Δ Complexity Δ
...java/com/nike/riposte/server/http/RequestInfo.java 100% <ø> (ø) 7 <0> (ø)
...server/channelpipeline/HttpChannelInitializer.java 100% <100%> (ø) 29 <0> (ø)
.../nike/riposte/server/http/ProxyRouterEndpoint.java 82.75% <100%> (+0.61%) 7 <1> (+1)
...in/java/com/nike/riposte/server/http/Endpoint.java 100% <100%> (ø) 9 <1> (+1)
...a/com/nike/riposte/server/config/ServerConfig.java 95.34% <100%> (ø) 34 <1> (ø)
...nike/riposte/server/http/impl/RequestInfoImpl.java 100% <100%> (ø) 64 <0> (ø)
...ckstopperRiposteFrameworkErrorHandlerListener.java 100% <100%> (ø) 25 <0> (+1)
...poste/server/handler/RequestInfoSetterHandler.java 88.88% <91.66%> (+12.41%) 13 <5> (+7)
.../com/nike/riposte/server/logging/AccessLogger.java 94.85% <0%> (-1.48%) 46% <0%> (-1%)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2fe765a...dfef3e7. Read the comment docs.

@@ -21,10 +24,19 @@
* This handler should come after {@link io.netty.handler.codec.http.HttpRequestDecoder} and {@link
* SmartHttpContentCompressor} in the pipeline.
*
* The request size is tracked and if it exceeds the configured global or a given endpoint's override, and exception
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor typo - "and exception" instead of "an exception".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good eyes. fixed.


private int getConfiguredMaxRequestSize(HttpProcessingState state) {
int maxConfiguredRequestSize = maxRequestSizeInBytes;
Optional<Integer> endpointMaxRequestSizeOverride = state.getEndpointForExecution().maxRequestSizeInBytesOverride();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state.getEndpointForExecution() can return null depending on the state of the request. A null endpoint should be considered the same as an empty optional, i.e. use the global default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it the endpoint could also return null. It would be a bad/broken endpoint but we still shouldn't NPE if someone does it. If null is returned from an endpoint's maxRequestSizeInBytesOverride() it should also be treated as an empty optional (i.e. use global default).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some more thinking about all this, here are some general refactors that I think should be done:

  1. The max request size in bytes (MRSB) was originally an int because once upon a time we used Netty's HttpObjectAggregator, and that handler did the MRSB check and required an int. We no longer have that restriction, so we should use a long instead to get rid of the 2GB ceiling on MRSB.
  2. Since we have to check for null on the endpoint override anyway even with Optional, and the core stuff tends to use nulls rather than optionals to avoid the perf hit of optionals, the Endpoint.maxRequestSizeInBytesOverride() method signature should probably change to just return a Long object. Returning null from the override method would mean "use the global default", and returning anything non-null would mean "use this value instead of the global default".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the details. I'll update the logic where it handles the endpoint or the endpoint's override as null and will return the global default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind on moving stuff to longs. As you pointed out, we're ultimately limited by the fact that byte[], ByteBuf, etc lengths are ints. So without some major refactoring to allow for exposing payloads as InputStreams or something similar we're stuck with an int limit.

* @author Nic Munroe
*/
public class RequestInfoSetterHandler extends BaseInboundHandlerWithTracingAndMdcSupport {

private final int maxRequestSizeInBytes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest renaming to defaultMaxRequestSizeInBytes or something else that indicates it's the default/global.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do: globalConfiguredMaxRequestSizeInBytes ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

.extract();

assertThat(response.statusCode()).isEqualTo(HttpResponseStatus.BAD_REQUEST.code());
assertThat(response.asString()).contains("Malformed request");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also look for the metadata here (and other places where we're verifying that the error was thrown).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call. i've added an assert for message and cause

.port(serverConfig.endpointsPort())
.basePath(BasicEndpoint.MATCHING_PATH)
.log().all()
.body("{'key1':'value1'}")
Copy link
Member

@nicmunroe nicmunroe Mar 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest that the global and endpoint-override max request size values be constants that can be referenced by these tests, and dynamically generate a request of the appropriate size in bytes relative to whatever constant you're testing against.

e.g. for this test:

.body(generatePayloadOfSizeInBytes(GLOBAL_MAX_REQUEST_SIZE + 1))

The dynamically generated payload doesn't have to be valid JSON - it can just be random characters, or all the same character, or whatever you want to do to make it easy - the only thing that matters is the size of the payload that's returned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good suggestion. I've incorporated this pattern in here.


// when
try {
handler.doChannelRead(ctxMock, httpContent);
Copy link
Member

@nicmunroe nicmunroe Mar 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AssertJ has a convenience method for all this:

Throwable thrownException = Assertions.catchThrowable(() -> handler.doChannelRead(ctxMock, httpContent));

Then you'd assert something on the exception which implicitly verifies the exception was thrown since if the assertion was executed on null it would also fail. The existing assertion in this test is sufficient, for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the tip. I've converted them over.

@Test
public void doChannelRead_allows_endpoints_to_override_max_request_size_setting_lower() {
// given
//handler is configured for 10
Copy link
Member

@nicmunroe nicmunroe Mar 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reference maxRequestSizeInBytes in the tests so you don't have to put these comments here and also removes the potential for accidental test breakage or worse, accidental regression in that the tests don't catch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call. I've moved this down in the tests so it is clearer the state of the test in the 'given'

/**
* @return The max request size in bytes that you want to allow for this specific endpoint, or Optional.empty()
* if you want to use the app-wide default max request size
* returned by {@link ServerConfig#maxRequestSizeInBytes()} ()}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing ()} that needs to be deleted. Also, the javadoc should include something about returning 0 to disable request size validation entirely.

@rabeyta rabeyta force-pushed the issue27 branch 2 times, most recently from 3f741cf to e9ae5ea Compare March 13, 2017 20:54
private static String generatePayloadOfSizeInBytes(int length) {
StringBuilder sb = new StringBuilder(length);
for(int i = 0; i < length; i++) {
sb.append(i);
Copy link
Member

@nicmunroe nicmunroe Mar 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For anything i >= 11 you'll be getting a string larger than the requested length in bytes. You could do sb.append(i % 10).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch.

.port(serverConfig.endpointsPort())
.basePath(BasicEndpoint.MATCHING_PATH)
.log().all()
.body(generatePayloadOfSizeInBytes(GLOBAL_MAX_REQUEST_SIZE - 1))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get rid of the - 1 I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call. its unnecessary. removed!

@@ -93,4 +119,102 @@ public void doChannelRead_checks_for_fully_send_responses_but_does_nothing_else_
assertThat(result).isEqualTo(PipelineContinuationBehavior.CONTINUE);
}

@Test
public void doChannelRead_returns_do_not_fire_when_null_state() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding these missing tests!

@@ -390,12 +390,11 @@ default int maxOpenIncomingServerChannels() {
/**
* @return The maximum allowed request size in bytes. If netty receives a request larger than this then it will
* throw a {@link io.netty.handler.codec.TooLongFrameException}.
* This value is an integer, so the max you can set it to is {@link Integer#MAX_VALUE}, which corresponds to 2^31-1,
Copy link
Member

@nicmunroe nicmunroe Mar 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave the comment about max size in here (but since you're fixing the NOTE that line should obviously go).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've added it back.

@@ -6,6 +6,8 @@
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Optional;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional import no longer needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i found the other, but missed this one!! Sorry about that.

…able this to be disabled or overridden on an endpoint by endpoint basis
@nicmunroe
Copy link
Member

Looks good, thank you for the PR!

@nicmunroe nicmunroe merged commit d89f801 into Nike-Inc:master Mar 13, 2017
@rabeyta rabeyta deleted the issue27 branch May 31, 2017 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants