Skip to content

Commit

Permalink
[fix][Undertow] Allow path parameters with encoded slashes to be deco…
Browse files Browse the repository at this point in the history
…ded (#163)

Servers may configure UndertowOptions.DECODE_URL to false in order
to support path parameters with query parameters.
  • Loading branch information
Carter Kozak authored and bulldozer-bot[bot] committed Jan 8, 2019
1 parent ac9dc66 commit 8e4f032
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
Expand Down Expand Up @@ -68,6 +69,12 @@ public interface EteService {
@Produces(MediaType.APPLICATION_OCTET_STREAM)
StreamingOutput binary(@HeaderParam("Authorization") @NotNull AuthHeader authHeader);

@GET
@Path("base/path/{param}")
String path(
@HeaderParam("Authorization") @NotNull AuthHeader authHeader,
@PathParam("param") String param);

@POST
@Path("base/notNullBody")
StringAliasExample notNullBody(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.palantir.tokens.auth.BearerToken;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.PathTemplateMatch;
import io.undertow.util.StatusCodes;
import java.io.IOException;
import java.time.OffsetDateTime;
Expand Down Expand Up @@ -65,6 +66,7 @@ public void register(RoutingRegistry routingRegistry) {
.get("/base/optionalEmpty", new OptionalEmptyHandler())
.get("/base/datetime", new DatetimeHandler())
.get("/base/binary", new BinaryHandler())
.get("/base/path/{param}", new PathHandler())
.post("/base/notNullBody", new NotNullBodyHandler())
.get("/base/aliasOne", new AliasOneHandler())
.get("/base/optionalAliasOne", new OptionalAliasOneHandler())
Expand Down Expand Up @@ -182,6 +184,18 @@ public void handleRequest(HttpServerExchange exchange) throws IOException {
}
}

private class PathHandler implements HttpHandler {
@Override
public void handleRequest(HttpServerExchange exchange) throws IOException {
AuthHeader authHeader = Auth.header(exchange);
Map<String, String> pathParams =
exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY).getParameters();
String param = StringDeserializers.deserializeString(pathParams.get("param"));
String result = delegate.path(authHeader, param);
serializers.serialize(result, exchange);
}
}

private class NotNullBodyHandler implements HttpHandler {
private final TypeToken<StringAliasExample> notNullBodyType =
new TypeToken<StringAliasExample>() {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;
import retrofit2.http.Streaming;

Expand Down Expand Up @@ -64,6 +65,10 @@ public interface EteServiceRetrofit {
@Streaming
Call<ResponseBody> binary(@Header("Authorization") AuthHeader authHeader);

@GET("./base/path/{param}")
@Headers({"hr-path-template: /base/path/{param}", "Accept: application/json"})
Call<String> path(@Header("Authorization") AuthHeader authHeader, @Path("param") String param);

@POST("./base/notNullBody")
@Headers({"hr-path-template: /base/notNullBody", "Accept: application/json"})
Call<StringAliasExample> notNullBody(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public interface UndertowEteService {

BinaryResponseBody binary(AuthHeader authHeader);

String path(AuthHeader authHeader, String param);

StringAliasExample notNullBody(AuthHeader authHeader, StringAliasExample notNullBody);

StringAliasExample aliasOne(AuthHeader authHeader, StringAliasExample queryParamName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ public Streaming binary(AuthHeader authHeader) {
return outputStream -> outputStream.write("Hello, world!".getBytes(StandardCharsets.UTF_8));
}

@Override
public String path(AuthHeader authHeader, String param) {
return param;
}

@Override
public StringAliasExample notNullBody(AuthHeader authHeader, StringAliasExample notNullBody) {
return notNullBody;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import com.palantir.tokens.auth.AuthHeader;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.UndertowOptions;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
Expand Down Expand Up @@ -127,6 +128,7 @@ public static void before() {
EteBinaryServiceEndpoint.of(new UndertowBinaryResource()));
endpoints.forEach(endpoint -> endpoint.create(context).register(handler));
server = Undertow.builder()
.setServerOption(UndertowOptions.DECODE_URL, false)
.addHttpListener(8080, "0.0.0.0")
.setHandler(Handlers.path().addPrefixPath("/test-example/api", handler))
.build();
Expand Down Expand Up @@ -329,6 +331,12 @@ public void testUnknownContentType() {
.hasMessageContaining("INVALID_ARGUMENT");
}

@Test
public void testSlashesInPathParam() {
String expected = "foo/bar/baz/%2F";
assertThat(client.path(AuthHeader.valueOf("bearer"), expected)).isEqualTo(expected);
}

@Test
public void testBinaryOptionalEmptyResponse() throws Exception {
URL url = new URL("http://0.0.0.0:8080/test-example/api/binary/optional/empty");
Expand Down
6 changes: 6 additions & 0 deletions conjure-java-core/src/test/resources/ete-service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ services:
http: GET /binary
returns: binary

path:
http: GET /path/{param}
args:
param: string
returns: string

notNullBody:
http: POST /notNullBody
args:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.undertow.server.RoutingHandler;
import io.undertow.server.handlers.BlockingHandler;
import io.undertow.server.handlers.ResponseCodeHandler;
import io.undertow.server.handlers.URLDecodingHandler;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import java.util.function.BiFunction;
Expand All @@ -36,6 +37,10 @@ public final class ConjureHandler implements HttpHandler, RoutingRegistry {

private static final ImmutableList<BiFunction<String, HttpHandler, HttpHandler>> WRAPPERS =
ImmutableList.<BiFunction<String, HttpHandler, HttpHandler>>of(
// Allow the server to configure UndertowOptions.DECODE_URL = false to allow slashes in parameters.
// Servers which do not configure DECODE_URL will still work properly except for encoded slash values.
(endpoint, handler) -> new URLDecodingHandler(handler, "UTF-8"),
(endpoint, handler) -> new PathParamDecodingHandler(handler),
// It is vitally important to never run blocking operations on the initial IO thread otherwise
// the server will not process new requests. all handlers executed after BlockingHandler
// use the larger task pool which is allowed to block. Any operation which sets thread
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.conjure.java.undertow.runtime;

import io.undertow.UndertowOptions;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.PathTemplateMatch;
import io.undertow.util.URLUtils;
import java.util.Map;

/**
* Undertow does not decode slashes in path parameters by default.
* This should be removed once Undertow 2.0.17.Final is released.
*
* @see <a href="https://issues.jboss.org/browse/UNDERTOW-1476">UNDERTOW-1476</a>
*/
class PathParamDecodingHandler implements HttpHandler {

private final HttpHandler next;

PathParamDecodingHandler(HttpHandler next) {
this.next = next;
}

@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
if (!isDecodingComplete(exchange)) {
PathTemplateMatch pathTemplateMatch = exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY);
if (pathTemplateMatch != null) {
Map<String, String> parameters = pathTemplateMatch.getParameters();
if (parameters != null && !parameters.isEmpty()) {
StringBuilder buffer = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) {
entry.setValue(URLUtils.decode(
entry.getValue(), "UTF-8", true, true, buffer));
}
}
}
}
next.handleRequest(exchange);
}

private static boolean isDecodingComplete(HttpServerExchange exchange) {
return exchange.getConnection()
.getUndertowOptions().get(UndertowOptions.DECODE_URL, true);
}
}

0 comments on commit 8e4f032

Please sign in to comment.