diff --git a/docs/modules/ROOT/examples/client-server/application.properties b/docs/modules/ROOT/examples/client-server/application.properties index 157860932..81c1b49c2 100644 --- a/docs/modules/ROOT/examples/client-server/application.properties +++ b/docs/modules/ROOT/examples/client-server/application.properties @@ -89,5 +89,38 @@ quarkus.cxf.client.largeSlow.service-interface = io.quarkiverse.cxf.it.large.slo #quarkus.cxf.codegen.wsdl2java.large-slow.package-names = io.quarkiverse.cxf.it.large.slow.generated #quarkus.cxf.codegen.wsdl2java.large-slow.additional-params = -b,src/main/resources/wsdl/LargeSlow-async-binding.xml +quarkus.cxf.client.singleRedirect.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/singleRedirect +quarkus.cxf.client.singleRedirect.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.singleRedirect.auto-redirect = true + +quarkus.cxf.client.doubleRedirect.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/doubleRedirect +quarkus.cxf.client.doubleRedirect.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.doubleRedirect.auto-redirect = true +# /RedirectRest/doubleRedirect redirects to the relative URI /RedirectRest/singleRedirect, so we have to allow that +quarkus.cxf.client.doubleRedirect.redirect-relative-uri = true + +quarkus.cxf.client.tripleRedirect.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/tripleRedirect +quarkus.cxf.client.tripleRedirect.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.tripleRedirect.auto-redirect = true +quarkus.cxf.client.tripleRedirect.redirect-relative-uri = true + +quarkus.cxf.client.noAutoRedirect.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/singleRedirect +quarkus.cxf.client.noAutoRedirect.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService + +quarkus.cxf.client.doubleRedirectMaxRetransmits1.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/doubleRedirect +quarkus.cxf.client.doubleRedirectMaxRetransmits1.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.doubleRedirectMaxRetransmits1.redirect-relative-uri = true +quarkus.cxf.client.doubleRedirectMaxRetransmits1.max-retransmits = 1 +quarkus.cxf.client.doubleRedirectMaxRetransmits1.auto-redirect = true + +quarkus.cxf.client.doubleRedirectMaxRetransmits2.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/doubleRedirect +quarkus.cxf.client.doubleRedirectMaxRetransmits2.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.doubleRedirectMaxRetransmits2.redirect-relative-uri = true +quarkus.cxf.client.doubleRedirectMaxRetransmits2.max-retransmits = 2 +quarkus.cxf.client.doubleRedirectMaxRetransmits2.auto-redirect = true + +quarkus.cxf.client.loop.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/loop1 +quarkus.cxf.client.loop.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.loop.auto-redirect = true quarkus.default-locale = en_US \ No newline at end of file diff --git a/docs/modules/ROOT/pages/reference/extensions/quarkus-cxf.adoc b/docs/modules/ROOT/pages/reference/extensions/quarkus-cxf.adoc index fe4e11e83..31d232e2e 100644 --- a/docs/modules/ROOT/pages/reference/extensions/quarkus-cxf.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/quarkus-cxf.adoc @@ -1655,11 +1655,39 @@ appliable). 0 is infinite. .<| `boolean` .<| `false` -3+a|Specifies if the consumer will automatically follow a server issued redirection. (name is not part of standard) +3+a|If `true` this CXF client will follow HTTP redirects (HTTP status codes 301, 302, 303 and 307 with `Location` response +header); otherwise HTTP redirects will not be followed. + +[WARNING] +==== +Enabling this option may increase memory requirements of your application substantially as request bodies will +have to be cached for retransmission. +==== + +See also: + +* `xref:reference/extensions/quarkus-cxf.adoc#quarkus-cxf_quarkus-cxf-client-client-name-redirect-relative-uri[quarkus.cxf.client."client-name".redirect-relative-uri]` +* `xref:reference/extensions/quarkus-cxf.adoc#quarkus-cxf_quarkus-cxf-client-client-name-max-retransmits[quarkus.cxf.client."client-name".max-retransmits]` *Environment variable*: `+++QUARKUS_CXF_CLIENT__CLIENT_NAME__AUTO_REDIRECT+++` + *Since Quarkus CXF*: 2.2.3 +.<| [[quarkus-cxf_quarkus-cxf-client-client-name-redirect-relative-uri]]`link:#quarkus-cxf_quarkus-cxf-client-client-name-redirect-relative-uri[quarkus.cxf.client."client-name".redirect-relative-uri]` +.<| `boolean` +.<| `false` + +3+a|If `true` relative URIs, such as `/folder/service` received via `Location` response header will be honored when +redirecting; otherwise only absolute URIs will be honored and an exception will be thrown for relative redirects. + +This is equivalent to setting `http.redirect.relative.uri` property to `true` on the CXF client request context. + +See also: + +* `xref:reference/extensions/quarkus-cxf.adoc#quarkus-cxf_quarkus-cxf-client-client-name-auto-redirect[quarkus.cxf.client."client-name".auto-redirect]` + +*Environment variable*: `+++QUARKUS_CXF_CLIENT__CLIENT_NAME__REDIRECT_RELATIVE_URI+++` + +*Since Quarkus CXF*: 3.17.0 + .<| [[quarkus-cxf_quarkus-cxf-client-client-name-max-retransmits]]`link:#quarkus-cxf_quarkus-cxf-client-client-name-max-retransmits[quarkus.cxf.client."client-name".max-retransmits]` .<| `int` .<| `-1` @@ -1669,6 +1697,10 @@ the retransmit count. Each redirect may cause another retransmit for a UNAUTHORI number indicates unlimited retransmits, although, loop protection is provided. The default is unlimited. (name is not part of standard) +See also: + +* `xref:reference/extensions/quarkus-cxf.adoc#quarkus-cxf_quarkus-cxf-client-client-name-auto-redirect[quarkus.cxf.client."client-name".auto-redirect]` + *Environment variable*: `+++QUARKUS_CXF_CLIENT__CLIENT_NAME__MAX_RETRANSMITS+++` + *Since Quarkus CXF*: 2.2.3 diff --git a/extensions/core/deployment/src/test/java/io/quarkiverse/cxf/deployment/test/Client3xx4xx5xxTest.java b/extensions/core/deployment/src/test/java/io/quarkiverse/cxf/deployment/test/Client3xx4xx5xxTest.java index fb3d70275..8f624732d 100644 --- a/extensions/core/deployment/src/test/java/io/quarkiverse/cxf/deployment/test/Client3xx4xx5xxTest.java +++ b/extensions/core/deployment/src/test/java/io/quarkiverse/cxf/deployment/test/Client3xx4xx5xxTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkiverse.cxf.annotation.CXFClient; +import io.quarkus.logging.Log; import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; import io.vertx.ext.web.Router; @@ -50,7 +51,14 @@ public class Client3xx4xx5xxTest { .overrideConfigKey("quarkus.cxf.client.endpointUri404.client-endpoint-url", "http://localhost:8081/services/no-such-service") .overrideConfigKey("quarkus.cxf.client.endpointUri404.service-interface", HelloService.class.getName()) - .overrideConfigKey("quarkus.cxf.client.endpointUri404.logging.enabled", "true"); + .overrideConfigKey("quarkus.cxf.client.endpointUri404.logging.enabled", "true") + + /* Bad service endpoint URI */ + .overrideConfigKey("quarkus.cxf.client.endpointUri302.client-endpoint-url", + "http://localhost:8081/vertx-redirect") + .overrideConfigKey("quarkus.cxf.client.endpointUri302.service-interface", HelloService.class.getName()) + .overrideConfigKey("quarkus.cxf.client.endpointUri302.auto-redirect", "true") + .overrideConfigKey("quarkus.cxf.client.endpointUri302.logging.enabled", "true"); @CXFClient("wsdlUri200") // Use Instance to avoid greedy initialization @@ -62,6 +70,9 @@ public class Client3xx4xx5xxTest { @CXFClient("endpointUri404") Instance endpointUri404; + @CXFClient("endpointUri302") + Instance endpointUri302; + Instance getClient(String clientName) { switch (clientName) { case "wsdlUri200": { @@ -73,6 +84,9 @@ Instance getClient(String clientName) { case "endpointUri404": { return endpointUri404; } + case "endpointUri302": { + return endpointUri302; + } default: throw new IllegalArgumentException("Unexpected client name: " + clientName); } @@ -97,7 +111,7 @@ void endpointUri404() { "HTTP response '404: Not Found' when communicating with http://localhost:8081/services/no-such-service"); } - public void init(@Observes Router router) { + void init(@Observes Router router) { router.route().handler(BodyHandler.create()); router.post("/vertx-blocking/:client").blockingHandler(ctx -> { final String person = ctx.body().asString(); @@ -114,6 +128,10 @@ public void init(@Observes Router router) { ctx.response().setStatusCode(500).end(r.getClass().getName() + " " + r.getMessage()); } }); + router.post("/vertx-redirect").handler(ctx -> { + Log.info("Redirecting"); + ctx.redirect("http://localhost:8081/services/hello"); + }); } @Test @@ -187,8 +205,17 @@ void endpointUri404OnEventLoop() throws InterruptedException { } + @Test + void endpointUri302OnWorkerThread() { + RestAssured.given() + .body("Joe") + .post("http://localhost:8081/vertx-blocking/endpointUri302") + .then() + .statusCode(200) + .body(Matchers.is("Hello Joe")); + } + private static Throwable rootCause(Exception e) { - e.printStackTrace(); Throwable result = e; while (result.getCause() != null) { result = result.getCause(); diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientInfo.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientInfo.java index b1ae5d086..08427ed8c 100644 --- a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientInfo.java +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientInfo.java @@ -70,6 +70,14 @@ public class CXFClientInfo { * (name is not part of standard) */ private final boolean autoRedirect; + + /** + * If `true` relative URIs, such as `/folder/service` received via `Location` response header will be honored when + * redirecting; otherwise only absolute URIs will be honored and an exception will be thrown for relative redirects. + * + * This is equivalent to setting `http.redirect.relative.uri` property to `true` on the CXF client request context. + */ + private final boolean redirectRelativeUri; /** * Specifies the maximum amount of retransmits that are allowed for redirects. Retransmits for * authorization is included in the retransmit count. Each redirect may cause another @@ -218,6 +226,7 @@ public CXFClientInfo(CXFClientData other, CxfConfig cxfConfig, CxfClientConfig c this.receiveTimeout = config.receiveTimeout(); this.connectionRequestTimeout = config.connectionRequestTimeout(); this.autoRedirect = config.autoRedirect(); + this.redirectRelativeUri = config.redirectRelativeUri(); this.maxRetransmits = config.maxRetransmits(); this.allowChunking = config.allowChunking(); this.chunkingThreshold = config.chunkingThreshold(); @@ -473,6 +482,10 @@ public boolean isAutoRedirect() { return autoRedirect; } + public boolean isRedirectRelativeUri() { + return redirectRelativeUri; + } + public int getMaxRetransmits() { return maxRetransmits; } diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientConfig.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientConfig.java index 097fe7b40..9a4a4b2a5 100644 --- a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientConfig.java +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientConfig.java @@ -187,24 +187,65 @@ public interface CxfClientConfig { @WithDefault("60000") public long connectionRequestTimeout(); + // The formatter breaks the list with long items + // @formatter:off /** - * Specifies if the consumer will automatically follow a server issued redirection. (name is not part of standard) + * If `true` this CXF client will follow HTTP redirects (HTTP status codes 301, 302, 303 and 307 with `Location` response + * header); otherwise HTTP redirects will not be followed. + * + * [WARNING] + * ==== + * Enabling this option may increase memory requirements of your application substantially as request bodies will + * have to be cached for retransmission. + * ==== + * + * See also: + * + * * `xref:reference/extensions/quarkus-cxf.adoc#quarkus-cxf_quarkus-cxf-client-client-name-redirect-relative-uri[quarkus.cxf.client."client-name".redirect-relative-uri]` + * * `xref:reference/extensions/quarkus-cxf.adoc#quarkus-cxf_quarkus-cxf-client-client-name-max-retransmits[quarkus.cxf.client."client-name".max-retransmits]` * * @since 2.2.3 * @asciidoclet */ + // @formatter:on @WithDefault("false") public boolean autoRedirect(); + // The formatter breaks the list with long items + // @formatter:off + /** + * If `true` relative URIs, such as `/folder/service` received via `Location` response header will be honored when + * redirecting; otherwise only absolute URIs will be honored and an exception will be thrown for relative redirects. + * + * This is equivalent to setting `http.redirect.relative.uri` property to `true` on the CXF client request context. + * + * See also: + * + * * `xref:reference/extensions/quarkus-cxf.adoc#quarkus-cxf_quarkus-cxf-client-client-name-auto-redirect[quarkus.cxf.client."client-name".auto-redirect]` + * + * @since 3.17.0 + * @asciidoclet + */ + // @formatter:on + @WithDefault("false") + public boolean redirectRelativeUri(); + + // The formatter breaks the list with long items + // @formatter:off /** * Specifies the maximum amount of retransmits that are allowed for redirects. Retransmits for authorization is included in * the retransmit count. Each redirect may cause another retransmit for a UNAUTHORIZED response code, ie. 401. Any negative * number indicates unlimited retransmits, although, loop protection is provided. The default is unlimited. (name is not * part of standard) * + * See also: + * + * * `xref:reference/extensions/quarkus-cxf.adoc#quarkus-cxf_quarkus-cxf-client-client-name-auto-redirect[quarkus.cxf.client."client-name".auto-redirect]` + * * @since 2.2.3 * @asciidoclet */ + // @formatter:on @WithDefault("-1") public int maxRetransmits(); diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientProducer.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientProducer.java index 611939700..3e0b4b995 100644 --- a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientProducer.java +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientProducer.java @@ -34,6 +34,7 @@ import io.quarkiverse.cxf.annotation.CXFClient; import io.quarkiverse.cxf.logging.LoggingFactoryCustomizer; import io.quarkiverse.cxf.vertx.http.client.HttpClientPool; +import io.quarkiverse.cxf.vertx.http.client.VertxHttpClientHTTPConduit; import io.vertx.core.Vertx; /** @@ -178,6 +179,9 @@ private Object produceCxfClient(CXFClientInfo cxfClientInfo) { props.put(WSAContextUtils.DECOUPLED_ENDPOINT_BASE_PROPERTY, value); } } + if (cxfClientInfo.isRedirectRelativeUri()) { + props.put(VertxHttpClientHTTPConduit.AUTO_REDIRECT_ALLOW_REL_URI, Boolean.TRUE); + } loggingFactoryCustomizer.customize(cxfClientInfo, factory); customizers.forEach(customizer -> customizer.customize(cxfClientInfo, factory)); diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/vertx/http/client/DummyBuffer.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/vertx/http/client/DummyBuffer.java new file mode 100644 index 000000000..2505a31f2 --- /dev/null +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/vertx/http/client/DummyBuffer.java @@ -0,0 +1,431 @@ +package io.quarkiverse.cxf.vertx.http.client; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import io.netty.buffer.ByteBuf; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +/** + * A buffer not backed by a Netty buffer so that it can be used in a static initializer without any complains from + * GraalVM + */ +class DummyBuffer implements Buffer { + + @Override + public void writeToBuffer(Buffer buffer) { + throw new UnsupportedOperationException(); + } + + @Override + public int readFromBuffer(int pos, Buffer buffer) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString(Charset enc) { + return ""; + } + + @Override + public String toString(String enc) { + return ""; + } + + @Override + public JsonObject toJsonObject() { + throw new UnsupportedOperationException(); + } + + @Override + public JsonArray toJsonArray() { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer slice(int start, int end) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer slice() { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setUnsignedShortLE(int pos, int s) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setUnsignedShort(int pos, int s) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setUnsignedIntLE(int pos, long i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setUnsignedInt(int pos, long i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setUnsignedByte(int pos, short b) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setString(int pos, String str, String enc) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setString(int pos, String str) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setShortLE(int pos, short s) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setShort(int pos, short s) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setMediumLE(int pos, int i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setMedium(int pos, int i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setLongLE(int pos, long l) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setLong(int pos, long l) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setIntLE(int pos, int i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setInt(int pos, int i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setFloat(int pos, float f) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setDouble(int pos, double d) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setBytes(int pos, byte[] b, int offset, int len) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setBytes(int pos, byte[] b) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setBytes(int pos, ByteBuffer b) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setByte(int pos, byte b) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setBuffer(int pos, Buffer b, int offset, int len) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer setBuffer(int pos, Buffer b) { + throw new UnsupportedOperationException(); + } + + @Override + public int length() { + return 0; + } + + @Override + public int getUnsignedShortLE(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public int getUnsignedShort(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public int getUnsignedMediumLE(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public int getUnsignedMedium(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public long getUnsignedIntLE(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public long getUnsignedInt(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public short getUnsignedByte(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public String getString(int start, int end, String enc) { + throw new UnsupportedOperationException(); + } + + @Override + public String getString(int start, int end) { + throw new UnsupportedOperationException(); + } + + @Override + public short getShortLE(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public short getShort(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public int getMediumLE(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public int getMedium(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public long getLongLE(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public long getLong(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public int getIntLE(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public int getInt(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public float getFloat(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public double getDouble(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer getBytes(int start, int end, byte[] dst, int dstIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer getBytes(int start, int end, byte[] dst) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer getBytes(byte[] dst, int dstIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getBytes(int start, int end) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer getBytes(byte[] dst) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getBytes() { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf getByteBuf() { + throw new UnsupportedOperationException(); + } + + @Override + public byte getByte(int pos) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer getBuffer(int start, int end) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer copy() { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendUnsignedShortLE(int s) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendUnsignedShort(int s) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendUnsignedIntLE(long i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendUnsignedInt(long i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendUnsignedByte(short b) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendString(String str, String enc) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendString(String str) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendShortLE(short s) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendShort(short s) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendMediumLE(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendMedium(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendLongLE(long l) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendLong(long l) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendIntLE(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendInt(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendFloat(float f) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendDouble(double d) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendBytes(byte[] bytes, int offset, int len) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendBytes(byte[] bytes) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendByte(byte b) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendBuffer(Buffer buff, int offset, int len) { + throw new UnsupportedOperationException(); + } + + @Override + public Buffer appendBuffer(Buffer buff) { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/vertx/http/client/VertxHttpClientHTTPConduit.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/vertx/http/client/VertxHttpClientHTTPConduit.java index b859ff975..81f8c7b89 100644 --- a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/vertx/http/client/VertxHttpClientHTTPConduit.java +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/vertx/http/client/VertxHttpClientHTTPConduit.java @@ -19,6 +19,8 @@ package io.quarkiverse.cxf.vertx.http.client; +import static io.vertx.core.http.HttpHeaders.CONTENT_LENGTH; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -29,8 +31,7 @@ import java.net.Proxy.Type; import java.net.SocketTimeoutException; import java.net.URI; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; +import java.net.URISyntaxException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -43,8 +44,11 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.stream.Collectors; import org.apache.cxf.Bus; +import org.apache.cxf.common.util.PropertyUtils; import org.apache.cxf.configuration.jsse.TLSClientParameters; import org.apache.cxf.endpoint.ClientCallback; import org.apache.cxf.endpoint.Endpoint; @@ -69,15 +73,14 @@ import org.eclipse.microprofile.context.ManagedExecutor; import org.jboss.logging.Logger; -import io.netty.buffer.ByteBuf; import io.quarkiverse.cxf.QuarkusTLSClientParameters; import io.quarkiverse.cxf.vertx.http.client.HttpClientPool.ClientSpec; import io.quarkiverse.cxf.vertx.http.client.VertxHttpClientHTTPConduit.RequestBodyEvent.RequestBodyEventType; import io.quarkus.arc.Arc; import io.quarkus.arc.InstanceHandle; +import io.quarkus.logging.Log; import io.quarkus.runtime.BlockingOperationControl; import io.vertx.core.AsyncResult; -import io.vertx.core.Context; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.MultiMap; @@ -90,8 +93,7 @@ import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpVersion; import io.vertx.core.http.RequestOptions; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; +import io.vertx.core.http.impl.HttpUtils; import io.vertx.core.net.ProxyOptions; import io.vertx.core.net.ProxyType; import io.vertx.core.streams.WriteStream; @@ -102,6 +104,10 @@ public class VertxHttpClientHTTPConduit extends HTTPConduit { private static final Logger log = Logger.getLogger(VertxHttpClientHTTPConduit.class); public static final String USE_ASYNC = "use.async.http.conduit"; public static final String ENABLE_HTTP2 = "org.apache.cxf.transports.http2.enabled"; + private static final String AUTO_REDIRECT_MAX_SAME_URI_COUNT = "http.redirect.max.same.uri.count"; + private static final String AUTO_REDIRECT_SAME_HOST_ONLY = "http.redirect.same.host.only"; + private static final String AUTO_REDIRECT_ALLOWED_URI = "http.redirect.allowed.uri"; + public static final String AUTO_REDIRECT_ALLOW_REL_URI = "http.redirect.relative.uri"; private final HttpClientPool httpClientPool; private final String userAgent; @@ -193,7 +199,9 @@ protected void setupConnection(Message message, Address address, HTTPClientPolic clientParameters.getTlsConfiguration()) : new ClientSpec(version, null, null), determineReceiveTimeout(message, csPolicy), - isAsync); + isAsync, + csPolicy.getMaxRetransmits(), + csPolicy.isAutoRedirect()); message.put(RequestContext.class, requestContext); } @@ -238,15 +246,20 @@ protected OutputStream createOutputStream( incomingObserver); final IOEHandler requestBodyHandler = new RequestBodyHandler( + getConduitName(), message, requestContext.uri, + cookies, userAgent, httpClientPool, requestContext.requestOptions, requestContext.clientSpec, requestContext.receiveTimeoutMs, responseHandler, - requestContext.async); + requestContext.async, + requestContext.autoRedirect, // we do not support authorizationRetransmit yet possibleRetransmit, + requestContext.maxRetransmits, + 0); return new RequestBodyOutputStream(chunkThreshold, requestBodyHandler); } @@ -328,7 +341,9 @@ static record RequestContext( RequestOptions requestOptions, ClientSpec clientSpec, long receiveTimeoutMs, - boolean async) { + boolean async, + int maxRetransmits, + boolean autoRedirect) { } static record RequestBodyEvent(Buffer buffer, RequestBodyEventType eventType) { @@ -410,6 +425,7 @@ public void close() throws IOException { static class RequestBodyHandler implements IOEHandler { private final Message outMessage; private final URI url; + private final Cookies cookies; private final String userAgent; private final HttpClientPool clientPool; private final RequestOptions requestOptions; @@ -423,6 +439,14 @@ static class RequestBodyHandler implements IOEHandler { */ private Result request; + /* Retransmit settings, read/written from the event loop */ + private final boolean possibleRetransmit; + private List bodyRecorder; + private List redirects; + private final int maxRetransmits; + private int performedRetransmits; + private final String conduitName; + /* Locks and conditions */ private final ReentrantLock lock = new ReentrantLock(); private final Condition requestReady = lock.newCondition(); @@ -434,18 +458,25 @@ static class RequestBodyHandler implements IOEHandler { private Mode mode; public RequestBodyHandler( + String conduitName, Message outMessage, URI url, + Cookies cookies, String userAgent, HttpClientPool clientPool, RequestOptions requestOptions, ClientSpec clientSpec, long receiveTimeoutMs, IOEHandler responseHandler, - boolean isAsync) { + boolean isAsync, + boolean possibleRetransmit, + int maxRetransmits, + int performedRetransmits) { super(); + this.conduitName = conduitName; this.outMessage = outMessage; this.url = url; + this.cookies = cookies; this.userAgent = userAgent; this.clientPool = clientPool; this.requestOptions = requestOptions; @@ -455,16 +486,28 @@ public RequestBodyHandler( this.mode = isAsync ? new Mode.Async(url, deadline, responseHandler, outMessage) : new Mode.Sync(url, deadline, responseHandler, lock); + + this.possibleRetransmit = possibleRetransmit; + this.maxRetransmits = maxRetransmits; + this.performedRetransmits = performedRetransmits; } @Override public void handle(RequestBodyEvent event) throws IOException { + + final Buffer buffer = event.buffer(); if (firstEvent) { firstEvent = false; - final HttpClient client = clientPool.getClient(clientSpec); + if (possibleRetransmit) { + final List recorder = bodyRecorder = new ArrayList<>(); + recorder.add(buffer.slice()); + final List redirs = redirects = new ArrayList<>(); + redirs.add(url); + } + final HttpClient client = clientPool.getClient(clientSpec); if (event.eventType() == RequestBodyEventType.COMPLETE_BODY && requestHasBody(requestOptions.getMethod())) { - requestOptions.putHeader("Content-Length", String.valueOf(event.buffer().length())); + requestOptions.putHeader(CONTENT_LENGTH, String.valueOf(buffer.length())); } setProtocolHeaders(outMessage, requestOptions, userAgent); @@ -475,7 +518,7 @@ public void handle(RequestBodyEvent event) throws IOException { case NON_FINAL_CHUNK: { req .setChunked(true) - .write(event.buffer()) + .write(buffer) .onFailure(t -> mode.responseFailed(t, true)); lock.lock(); @@ -490,7 +533,7 @@ public void handle(RequestBodyEvent event) throws IOException { } case FINAL_CHUNK: case COMPLETE_BODY: { - finishRequest(req, event.buffer()); + finishRequest(req, buffer); break; } default: @@ -529,17 +572,20 @@ public void handle(RequestBodyEvent event) throws IOException { } else { /* Non-first event */ + if (bodyRecorder != null) { + bodyRecorder.add(buffer.slice()); + } final HttpClientRequest req = awaitRequest(); switch (event.eventType()) { case NON_FINAL_CHUNK: { req - .write(event.buffer()) + .write(buffer) .onFailure(RequestBodyHandler.this::failResponse); break; } case FINAL_CHUNK: case COMPLETE_BODY: { - finishRequest(req, event.buffer()); + finishRequest(req, buffer); mode.awaitResponse(); break; } @@ -551,13 +597,74 @@ public void handle(RequestBodyEvent event) throws IOException { } } + @SuppressWarnings("resource") void finishRequest(HttpClientRequest req, Buffer buffer) { req.response() .onComplete(ar -> { final InputStreamWriteStream sink = new InputStreamWriteStream(2); + final HttpClientResponse response = ar.result(); if (ar.succeeded()) { - ar.result().pipeTo(sink); + + /* need to retransmit? */ + final boolean isRedirect = isRedirect(response.statusCode()); + if (possibleRetransmit + && (maxRetransmits < 0 || performedRetransmits < maxRetransmits) + && isRedirect) { + performedRetransmits++; + ResponseHandler.updateResponseHeaders(response, outMessage, cookies); + final String loc = response.getHeader("Location"); + try { + + if (loc != null && !loc.startsWith("http") + && !MessageUtils.getContextualBoolean(outMessage, AUTO_REDIRECT_ALLOW_REL_URI)) { + throw new IOException( + "Relative Redirect detected on Conduit '" + conduitName + "' on '" + loc + "'." + + " You may want to set quarkus.cxf.client.\"client-name\".redirect-relative-uri = true," + + " where \"client-name\" is the name of your client in application.properties"); + } + final URI previousUri = redirects.get(redirects.size() - 1); + final URI newUri = HttpUtils.resolveURIReference(previousUri, loc); + detectRedirectLoop(conduitName, redirects, newUri, outMessage); + redirects.add(newUri); + checkAllowedRedirectUri(conduitName, previousUri, newUri, outMessage); + redirectRetransmit(newUri); + } catch (IOException e) { + sink.setException((IOException) e); + mode.responseReady(new Result<>(new ResponseEvent(response, sink), ar.cause())); + } catch (URISyntaxException e) { + sink.setException(new IOException( + "Could not resolve redirect Location " + loc + " relative to " + url, e)); + mode.responseReady(new Result<>(new ResponseEvent(response, sink), ar.cause())); + } catch (Exception e) { + sink.setException(new IOException(e)); + mode.responseReady(new Result<>(new ResponseEvent(response, sink), ar.cause())); + } + return; + } else { + if (!possibleRetransmit && isRedirect) { + Log.warnf( + "Received redirection status %d from %s, but following redirects is not" + + " enabled for this CXF client. You may want to set" + + " quarkus.cxf.client.\"client-name\".auto-redirect = true," + + " where \"client-name\" is the name of your client in application.properties", + response.statusCode(), + url); + } + if (possibleRetransmit && isRedirect && maxRetransmits >= 0 + && maxRetransmits <= performedRetransmits) { + Log.warnf( + "Received redirection status %d from %s, but already performed maximum" + + " number %d of allowed retransmits for this exchange; you may want to" + + " increase quarkus.cxf.client.\"client-name\".max-retransmits in application.properties", + response.statusCode(), + redirects.get(redirects.size() - 1), + maxRetransmits); + } + /* No retransmit */ + /* Pass the body back to CXF */ + response.pipeTo(sink); + } } else { if (ar.cause() instanceof IOException) { sink.setException((IOException) ar.cause()); @@ -565,8 +672,11 @@ void finishRequest(HttpClientRequest req, Buffer buffer) { sink.setException(new IOException(ar.cause())); } } - mode.responseReady(new Result<>(new ResponseEvent(ar.result(), sink), ar.cause())); + mode.responseReady(new Result<>(new ResponseEvent(response, sink), ar.cause())); }); + if (bodyRecorder != null) { + bodyRecorder.add(buffer.slice()); + } req .end(buffer) @@ -574,6 +684,146 @@ void finishRequest(HttpClientRequest req, Buffer buffer) { } + void redirectRetransmit(URI newURL) throws IOException { + if (Log.isDebugEnabled()) { + Log.debugf("Redirect retransmit from %s to %s", redirects.get(redirects.size() - 1), newURL); + } + boolean ssl; + int port = newURL.getPort(); + String protocol = newURL.getScheme(); + char chend = protocol.charAt(protocol.length() - 1); + if (chend == 'p') { + ssl = false; + if (port == -1) { + port = 80; + } + } else if (chend == 's') { + ssl = true; + if (port == -1) { + port = 443; + } + } else { + throw new IllegalStateException("Unexpected URI scheme " + protocol + "; expected 'http' or 'https'"); + } + String requestURI = newURL.getPath(); + if (requestURI == null || requestURI.isEmpty()) { + requestURI = "/"; + } + String query = newURL.getQuery(); + if (query != null) { + requestURI += "?" + query; + } + RequestOptions options = new RequestOptions(requestOptions); + options.setHost(newURL.getHost()); + options.setPort(port); + options.setSsl(ssl); + options.setURI(requestURI); + + final List body = bodyRecorder; + final int last = body.size() - 1; + if (last == 0 && requestHasBody(options.getMethod())) { + /* Only one buffer recorded */ + requestOptions.putHeader(CONTENT_LENGTH, String.valueOf(body.get(0).length())); + } else if (last == -1 && requestHasBody(options.getMethod())) { + /* No buffer recorded */ + requestOptions.putHeader(CONTENT_LENGTH, "0"); + } else { + options.removeHeader(CONTENT_LENGTH); + } + + final HttpClient client = clientPool.getClient(clientSpec); + + // Should not be necessary, because we copy from the original requestOptions + // setProtocolHeaders(outMessage, options, userAgent); + + client.request(options) + .onSuccess(req -> { + if (last == 0) { + /* Single buffer recorded */ + finishRequest(req, body.get(0).slice()); + } else if (last == -1) { + /* Empty body */ + finishRequest(req, Buffer.buffer()); + } else { + /* Multiple buffers recorded */ + req.setChunked(true); + for (int i = 0; i <= last; i++) { + if (i == last) { + finishRequest(req, body.get(i).slice()); + } else { + req + .write(body.get(i).slice()) + .onFailure(t -> mode.responseFailed(t, true)); + } + } + } + }) + .onFailure(t -> { + lock.lock(); + try { + request = Result.failure(t); + requestReady.signal(); + + /* Fail also the response so that awaitResponse() fails rather than waiting forever */ + mode.responseFailed(t, false); + } finally { + lock.unlock(); + } + }); + } + + private static boolean isRedirect(int statusCode) { + return statusCode >= 301 // fast return for statusCode == 200 that we'll see mostly + && (statusCode == 302 || statusCode == 301 || statusCode == 303 || statusCode == 307); + } + + private static void detectRedirectLoop(String conduitName, + List redirects, + URI newURL, + Message message) throws IOException { + if (redirects.contains(newURL)) { + final Integer maxSameURICount = PropertyUtils.getInteger(message, AUTO_REDIRECT_MAX_SAME_URI_COUNT); + if (maxSameURICount != null + && redirects.stream().filter(newURL::equals).count() > maxSameURICount.longValue()) { + final String msg = "Redirect loop detected on Conduit '" + + conduitName + "' (with http.redirect.max.same.uri.count = " + maxSameURICount + "): " + + redirects.stream().map(URI::toString).collect(Collectors.joining(" -> ")) + " -> " + newURL; + throw new IOException(msg); + } else { + final String msg = "Redirect loop detected on Conduit '" + + conduitName + "': " + redirects.stream().map(URI::toString).collect(Collectors.joining(" -> ")) + + " -> " + newURL; + throw new IOException(msg); + } + } + } + + private static void checkAllowedRedirectUri(String conduitName, + URI lastUri, + URI newUri, + Message message) throws IOException { + if (MessageUtils.getContextualBoolean(message, AUTO_REDIRECT_SAME_HOST_ONLY)) { + + // This can be further restricted to make sure newURL completely contains lastURL + // though making sure the same HTTP scheme and host are preserved should be enough + + if (!newUri.getScheme().equals(lastUri.getScheme()) + || !newUri.getHost().equals(lastUri.getHost())) { + String msg = "Different HTTP Scheme or Host Redirect detected on Conduit '" + + conduitName + "' on '" + newUri + "'"; + LOG.log(Level.INFO, msg); + throw new IOException(msg); + } + } + + String allowedRedirectURI = (String) message.getContextualProperty(AUTO_REDIRECT_ALLOWED_URI); + if (allowedRedirectURI != null && !newUri.toString().startsWith(allowedRedirectURI)) { + String msg = "Forbidden Redirect URI " + newUri + "detected on Conduit '" + conduitName; + LOG.log(Level.INFO, msg); + throw new IOException(msg); + } + } + void failResponse(Throwable t) { } @@ -681,7 +931,7 @@ void awaitWriteable(HttpClientRequest request) throws IOException, InterruptedEx if (this.request.cause() != null) { throw new IOException(this.request.cause()); } - if (Context.isOnEventLoopThread()) { + if (!BlockingOperationControl.isBlockingAllowed()) { throw new IllegalStateException("Attempting a blocking write on io thread"); } if (!drainHandlerRegistered) { @@ -762,7 +1012,7 @@ static class Sync extends Mode { } @Override - public void responseFailed(Throwable t, boolean lockIfNeeded) { + protected void responseFailed(Throwable t, boolean lockIfNeeded) { if (lockIfNeeded) { lock.lock(); try { @@ -779,7 +1029,7 @@ public void responseFailed(Throwable t, boolean lockIfNeeded) { } @Override - public void responseReady(Result response) { + protected void responseReady(Result response) { lock.lock(); try { this.response = response; @@ -790,7 +1040,7 @@ public void responseReady(Result response) { } @Override - public void awaitResponse() throws IOException { + protected void awaitResponse() throws IOException { responseHandler.handle(awaitResponseInternal()); } @@ -922,7 +1172,8 @@ public ResponseHandler(URI url, Message outMessage, Cookies cookies, MessageObse public void handle(ResponseEvent responseEvent) throws IOException { final HttpClientResponse response = responseEvent.response; final Exchange exchange = outMessage.getExchange(); - final int responseCode = doProcessResponseCode(url, response, exchange, outMessage); + final URI uri = URI.create(response.request().absoluteURI()); + final int responseCode = doProcessResponseCode(uri, response, exchange, outMessage); InputStream in = null; // oneway or decoupled twoway calls may expect HTTP 202 with no content @@ -1152,8 +1403,9 @@ public void write(Buffer data, Handler> handler) { lock.lock(); try { queue.offer(data); - // Log.infof("Adding buffer %d with size %d bytes; queue size after %d", System.identityHashCode(data), - // data.length(), queue.size()); + // Log.infof("Adding buffer %d with size %d bytes; queue size after %d", + // System.identityHashCode(data), + // data.length(), queue.size()); queueChange.signal(); } finally { lock.unlock(); @@ -1250,10 +1502,13 @@ public int read(byte b[], final int off, int len) throws IOException { } result = readable; } else { - /* readable < len so we read out the current buffer completely and we try the subsequent ones if available */ + /* + * readable < len so we read out the current buffer completely and we try the subsequent ones if + * available + */ rb.getBytes(readPosition, readPosition + readable, b, off); // Log.infof("Read out current buffer %d completely: %s", System.identityHashCode(rb), - // new String(b, off, readable, StandardCharsets.UTF_8)); + // new String(b, off, readable, StandardCharsets.UTF_8)); readPosition += readable; // assert readPosition == rbLen; len -= readable; @@ -1270,8 +1525,8 @@ public int read(byte b[], final int off, int len) throws IOException { } rb.getBytes(readPosition, readPosition + readable, b, off2); // Log.infof("Read 2 from buffer %d %d to %d: %s", System.identityHashCode(rb), readPosition, - // readPosition + readable, - // new String(b, off2, readable, StandardCharsets.UTF_8)); + // readPosition + readable, + // new String(b, off2, readable, StandardCharsets.UTF_8)); readPosition += readable; len -= readable; off2 += readable; @@ -1320,13 +1575,13 @@ public int available() throws IOException { private Buffer takeBuffer(boolean blockingAwaitBuffer) throws IOException { // Log.infof("About to take buffer at queue size %d %s", queue.size(), - // blockingAwaitBuffer ? "with blocking" : "without blocking"); + // blockingAwaitBuffer ? "with blocking" : "without blocking"); Buffer rb = readBuffer; if (rb == END) { return null; } // Log.infof("Buffer is null: %s; %d >= %d: %s", rb == null, readPosition, (rb == null ? -1 : rb.length()), - // rb != null && readPosition >= rb.length()); + // rb != null && readPosition >= rb.length()); if (rb == null || readPosition >= rb.length()) { // Log.info("Buffer is null or empty"); @@ -1335,7 +1590,7 @@ private Buffer takeBuffer(boolean blockingAwaitBuffer) throws IOException { lock.lockInterruptibly(); if (blockingAwaitBuffer) { while ((readBuffer = rb = queue.poll()) == null) { - //Log.infof("Awaiting a buffer at queue size %d", queue.size()); + // Log.infof("Awaiting a buffer at queue size %d", queue.size()); queueChange.await(); } } else { @@ -1358,22 +1613,12 @@ private Buffer takeBuffer(boolean blockingAwaitBuffer) throws IOException { } } // Log.infof("Taken a %s buffer %d, will read from %d to %d; queue size after: %d", - // (rb != null ? "valid" : "null"), - // System.identityHashCode(rb), - // readPosition, (rb != null ? rb.length() : -1), queue.size()); + // (rb != null ? "valid" : "null"), + // System.identityHashCode(rb), + // readPosition, (rb != null ? rb.length() : -1), queue.size()); return rb; } - private boolean queueEmpty() { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - return queue.isEmpty(); - } finally { - lock.unlock(); - } - } - public void setException(IOException exception) { if (this.exception == null) { /* Ignore subsequent exceptions */ @@ -1383,424 +1628,6 @@ public void setException(IOException exception) { } - static class DummyBuffer implements Buffer { - - @Override - public void writeToBuffer(Buffer buffer) { - throw new UnsupportedOperationException(); - } - - @Override - public int readFromBuffer(int pos, Buffer buffer) { - throw new UnsupportedOperationException(); - } - - @Override - public String toString(Charset enc) { - return ""; - } - - @Override - public String toString(String enc) { - return ""; - } - - @Override - public JsonObject toJsonObject() { - throw new UnsupportedOperationException(); - } - - @Override - public JsonArray toJsonArray() { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer slice(int start, int end) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer slice() { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setUnsignedShortLE(int pos, int s) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setUnsignedShort(int pos, int s) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setUnsignedIntLE(int pos, long i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setUnsignedInt(int pos, long i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setUnsignedByte(int pos, short b) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setString(int pos, String str, String enc) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setString(int pos, String str) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setShortLE(int pos, short s) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setShort(int pos, short s) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setMediumLE(int pos, int i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setMedium(int pos, int i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setLongLE(int pos, long l) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setLong(int pos, long l) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setIntLE(int pos, int i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setInt(int pos, int i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setFloat(int pos, float f) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setDouble(int pos, double d) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setBytes(int pos, byte[] b, int offset, int len) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setBytes(int pos, byte[] b) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setBytes(int pos, ByteBuffer b) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setByte(int pos, byte b) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setBuffer(int pos, Buffer b, int offset, int len) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer setBuffer(int pos, Buffer b) { - throw new UnsupportedOperationException(); - } - - @Override - public int length() { - return 0; - } - - @Override - public int getUnsignedShortLE(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public int getUnsignedShort(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public int getUnsignedMediumLE(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public int getUnsignedMedium(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public long getUnsignedIntLE(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public long getUnsignedInt(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public short getUnsignedByte(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public String getString(int start, int end, String enc) { - throw new UnsupportedOperationException(); - } - - @Override - public String getString(int start, int end) { - throw new UnsupportedOperationException(); - } - - @Override - public short getShortLE(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public short getShort(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public int getMediumLE(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public int getMedium(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public long getLongLE(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public long getLong(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public int getIntLE(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public int getInt(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public float getFloat(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public double getDouble(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer getBytes(int start, int end, byte[] dst, int dstIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer getBytes(int start, int end, byte[] dst) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer getBytes(byte[] dst, int dstIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public byte[] getBytes(int start, int end) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer getBytes(byte[] dst) { - throw new UnsupportedOperationException(); - } - - @Override - public byte[] getBytes() { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf getByteBuf() { - throw new UnsupportedOperationException(); - } - - @Override - public byte getByte(int pos) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer getBuffer(int start, int end) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer copy() { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendUnsignedShortLE(int s) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendUnsignedShort(int s) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendUnsignedIntLE(long i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendUnsignedInt(long i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendUnsignedByte(short b) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendString(String str, String enc) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendString(String str) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendShortLE(short s) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendShort(short s) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendMediumLE(int i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendMedium(int i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendLongLE(long l) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendLong(long l) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendIntLE(int i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendInt(int i) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendFloat(float f) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendDouble(double d) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendBytes(byte[] bytes, int offset, int len) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendBytes(byte[] bytes) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendByte(byte b) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendBuffer(Buffer buff, int offset, int len) { - throw new UnsupportedOperationException(); - } - - @Override - public Buffer appendBuffer(Buffer buff) { - throw new UnsupportedOperationException(); - } - } - public interface IOEHandler { /** diff --git a/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/VerboseExceptionMapper.java b/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/VerboseExceptionMapper.java new file mode 100644 index 000000000..dc81d3916 --- /dev/null +++ b/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/VerboseExceptionMapper.java @@ -0,0 +1,30 @@ +package io.quarkiverse.cxf.it; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +@Priority(10000) +//the more generic the mapper, the lower its prio should be(the lower the number the higher the pri), so you can override it with more specific mappers +public class VerboseExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(Exception e) { + return Response + .serverError() + .entity(rootCause(e).getMessage()) + .build(); + } + + private static Throwable rootCause(Throwable e) { + e.printStackTrace(); + Throwable result = e; + while (result.getCause() != null) { + result = result.getCause(); + } + return result; + } + +} \ No newline at end of file diff --git a/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/large/slow/LargeSlowServiceImpl.java b/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/large/slow/LargeSlowServiceImpl.java index b298350fa..cc64a1216 100644 --- a/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/large/slow/LargeSlowServiceImpl.java +++ b/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/large/slow/LargeSlowServiceImpl.java @@ -17,12 +17,16 @@ public class LargeSlowServiceImpl implements LargeSlowService { @Override public LargeSlowOutput largeSlow(int sizeBytes, int delayMs) { + return new LargeSlowOutput(delayMs, largeString(sizeBytes)); + } + + public static String largeString(int sizeBytes) { final StringBuilder sb = new StringBuilder(); while (sb.length() < sizeBytes) { sb.append("0123456789"); } sb.setLength(sizeBytes); - return new LargeSlowOutput(delayMs, sb.toString()); + return sb.toString(); } @Override diff --git a/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/large/slow/generated/LargeSlowOutput.java b/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/large/slow/generated/LargeSlowOutput.java index 35f45f651..bd31cca4b 100644 --- a/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/large/slow/generated/LargeSlowOutput.java +++ b/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/large/slow/generated/LargeSlowOutput.java @@ -61,11 +61,13 @@ public int getDelayMs() { @XmlElement(name = "delayMs") public void setDelayMs(int delayMs) { Log.infof("Sleeping for %d ms", delayMs); - try { - Thread.sleep(delayMs); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); + if (delayMs > 0) { + try { + Thread.sleep(delayMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } } this.delayMs = delayMs; } diff --git a/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/redirect/RedirectRest.java b/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/redirect/RedirectRest.java new file mode 100644 index 000000000..21fbdce59 --- /dev/null +++ b/integration-tests/client-server/src/main/java/io/quarkiverse/cxf/it/redirect/RedirectRest.java @@ -0,0 +1,125 @@ +package io.quarkiverse.cxf.it.redirect; + +import java.net.URI; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import io.quarkiverse.cxf.annotation.CXFClient; +import io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService; +import io.smallrye.mutiny.Uni; + +@Path("/RedirectRest") +public class RedirectRest { + + @CXFClient("singleRedirect") + LargeSlowService singleRedirect; + + @CXFClient("doubleRedirect") + LargeSlowService doubleRedirect; + + @CXFClient("tripleRedirect") + LargeSlowService tripleRedirect; + + @CXFClient("noAutoRedirect") + LargeSlowService noAutoRedirect; + + @CXFClient("doubleRedirectMaxRetransmits1") + LargeSlowService doubleRedirectMaxRetransmits1; + + @CXFClient("doubleRedirectMaxRetransmits2") + LargeSlowService doubleRedirectMaxRetransmits2; + + @CXFClient("loop") + LargeSlowService loop; + + LargeSlowService getClient(String clientName) { + switch (clientName) { + case "singleRedirect": { + return singleRedirect; + } + case "doubleRedirect": { + return doubleRedirect; + } + case "tripleRedirect": { + return tripleRedirect; + } + case "noAutoRedirect": { + return noAutoRedirect; + } + case "doubleRedirectMaxRetransmits1": { + return doubleRedirectMaxRetransmits1; + } + case "doubleRedirectMaxRetransmits2": { + return doubleRedirectMaxRetransmits2; + } + case "loop": { + return loop; + } + default: + throw new IllegalArgumentException("Unexpected client name: " + clientName); + } + } + + @Path("/singleRedirect") + @POST + public Response singleRedirect(String body) { + // Log.infof("/RedirectRest/singleRedirect Received payload %s", body); + // Relative URI: return Response.status(302).header("Location", "/soap/largeSlow").build(); + // RestEASY will convert this to an absolute URI + return Response.temporaryRedirect(URI.create("/soap/largeSlow")).build(); + } + + @Path("/doubleRedirect") + @POST + public Response doubleRedirect(String body) { + // Log.infof("/RedirectRest/doubleRedirect Received payload %s", body); + return Response.status(302).header("Location", "/RedirectRest/singleRedirect").build(); + } + + @Path("/tripleRedirect") + @POST + public Response tripleRedirect() { + return Response.status(302).header("Location", "/RedirectRest/doubleRedirect").build(); + } + + @Path("/loop1") + @POST + public Response loop1() { + return Response.temporaryRedirect(URI.create("/RedirectRest/loop2")).build(); + } + + @Path("/loop2") + @POST + public Response loop2() { + // Log.infof("/RedirectRest/singleRedirect Received payload %s", body); + // Relative URI: return Response.status(302).header("Location", "/soap/largeSlow").build(); + // RestEASY will convert this to an absolute URI + return Response.temporaryRedirect(URI.create("/RedirectRest/loop1")).build(); + } + + @Path("/async/{client}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public Uni async(@PathParam("client") String client, @QueryParam("sizeBytes") int sizeBytes, + @QueryParam("delayMs") int delayMs) { + return Uni.createFrom() + .future(getClient(client).largeSlowAsync(sizeBytes, delayMs)) + .map(addResponse -> addResponse.getReturn().getPayload()); + } + + @Path("/sync/{client}") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String largeHelloSync(@PathParam("client") String client, @QueryParam("sizeBytes") int sizeBytes, + @QueryParam("delayMs") int delayMs) { + return getClient(client).largeSlow(sizeBytes, delayMs).getPayload(); + } + +} diff --git a/integration-tests/client-server/src/main/resources/application.properties b/integration-tests/client-server/src/main/resources/application.properties index 157860932..81c1b49c2 100644 --- a/integration-tests/client-server/src/main/resources/application.properties +++ b/integration-tests/client-server/src/main/resources/application.properties @@ -89,5 +89,38 @@ quarkus.cxf.client.largeSlow.service-interface = io.quarkiverse.cxf.it.large.slo #quarkus.cxf.codegen.wsdl2java.large-slow.package-names = io.quarkiverse.cxf.it.large.slow.generated #quarkus.cxf.codegen.wsdl2java.large-slow.additional-params = -b,src/main/resources/wsdl/LargeSlow-async-binding.xml +quarkus.cxf.client.singleRedirect.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/singleRedirect +quarkus.cxf.client.singleRedirect.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.singleRedirect.auto-redirect = true + +quarkus.cxf.client.doubleRedirect.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/doubleRedirect +quarkus.cxf.client.doubleRedirect.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.doubleRedirect.auto-redirect = true +# /RedirectRest/doubleRedirect redirects to the relative URI /RedirectRest/singleRedirect, so we have to allow that +quarkus.cxf.client.doubleRedirect.redirect-relative-uri = true + +quarkus.cxf.client.tripleRedirect.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/tripleRedirect +quarkus.cxf.client.tripleRedirect.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.tripleRedirect.auto-redirect = true +quarkus.cxf.client.tripleRedirect.redirect-relative-uri = true + +quarkus.cxf.client.noAutoRedirect.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/singleRedirect +quarkus.cxf.client.noAutoRedirect.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService + +quarkus.cxf.client.doubleRedirectMaxRetransmits1.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/doubleRedirect +quarkus.cxf.client.doubleRedirectMaxRetransmits1.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.doubleRedirectMaxRetransmits1.redirect-relative-uri = true +quarkus.cxf.client.doubleRedirectMaxRetransmits1.max-retransmits = 1 +quarkus.cxf.client.doubleRedirectMaxRetransmits1.auto-redirect = true + +quarkus.cxf.client.doubleRedirectMaxRetransmits2.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/doubleRedirect +quarkus.cxf.client.doubleRedirectMaxRetransmits2.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.doubleRedirectMaxRetransmits2.redirect-relative-uri = true +quarkus.cxf.client.doubleRedirectMaxRetransmits2.max-retransmits = 2 +quarkus.cxf.client.doubleRedirectMaxRetransmits2.auto-redirect = true + +quarkus.cxf.client.loop.client-endpoint-url = http://localhost:${quarkus.http.test-port}/RedirectRest/loop1 +quarkus.cxf.client.loop.service-interface = io.quarkiverse.cxf.it.large.slow.generated.LargeSlowService +quarkus.cxf.client.loop.auto-redirect = true quarkus.default-locale = en_US \ No newline at end of file diff --git a/integration-tests/client-server/src/test/java/io/quarkiverse/cxf/it/redirect/RedirectIT.java b/integration-tests/client-server/src/test/java/io/quarkiverse/cxf/it/redirect/RedirectIT.java new file mode 100644 index 000000000..e240e4234 --- /dev/null +++ b/integration-tests/client-server/src/test/java/io/quarkiverse/cxf/it/redirect/RedirectIT.java @@ -0,0 +1,8 @@ +package io.quarkiverse.cxf.it.redirect; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class RedirectIT extends RedirectTest { + +} diff --git a/integration-tests/client-server/src/test/java/io/quarkiverse/cxf/it/redirect/RedirectTest.java b/integration-tests/client-server/src/test/java/io/quarkiverse/cxf/it/redirect/RedirectTest.java new file mode 100644 index 000000000..e5dbb828f --- /dev/null +++ b/integration-tests/client-server/src/test/java/io/quarkiverse/cxf/it/redirect/RedirectTest.java @@ -0,0 +1,100 @@ +package io.quarkiverse.cxf.it.redirect; + +import java.util.concurrent.ExecutionException; + +import org.assertj.core.api.Assumptions; +import org.hamcrest.CoreMatchers; +import org.hamcrest.Matchers; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkiverse.cxf.HTTPConduitImpl; +import io.quarkiverse.cxf.it.large.slow.LargeSlowServiceImpl; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.response.ValidatableResponse; + +@QuarkusTest +class RedirectTest { + + @ParameterizedTest + @ValueSource(strings = { // + "sync/singleRedirect", // + "async/singleRedirect", // + + "sync/doubleRedirect", // + "async/doubleRedirect", // + + "sync/tripleRedirect", // + "async/tripleRedirect", // + + "sync/doubleRedirectMaxRetransmits2", // + "async/doubleRedirectMaxRetransmits2" // + + }) + void redirect(String endpoint) throws InterruptedException, ExecutionException { + int sizeBytes = 16; // smallish, suits single buffer in + // VertxHttpClientHTTPConduit.RequestBodyHandler.bodyRecorder + getResponse(endpoint, sizeBytes) + .statusCode(200) + .body(Matchers.is(LargeSlowServiceImpl.largeString(sizeBytes))); + + sizeBytes = 9 * 1024; // biggish, forces multiple buffers in + // VertxHttpClientHTTPConduit.RequestBodyHandler.bodyRecorder + getResponse(endpoint, sizeBytes) + .statusCode(200) + .body(Matchers.is(LargeSlowServiceImpl.largeString(sizeBytes))); + } + + @ParameterizedTest + @ValueSource(strings = { // + "sync/noAutoRedirect", // + "async/noAutoRedirect" // + }) + void noAutoRedirect(String endpoint) { + int sizeBytes = 16; + getResponse(endpoint, sizeBytes) + .statusCode(500) + .body(CoreMatchers.containsString("Unexpected EOF in prolog")); + } + + @ParameterizedTest + @ValueSource(strings = { // + "sync/doubleRedirectMaxRetransmits1", // + "async/doubleRedirectMaxRetransmits1" // + }) + void doubleRedirectMaxRetransmits1(String endpoint) { + int sizeBytes = 16; + getResponse(endpoint, sizeBytes) + .statusCode(500) + .body(CoreMatchers.containsString("Unexpected EOF in prolog")); + } + + @ParameterizedTest + @ValueSource(strings = { // + "sync/loop", // + "async/loop" // + }) + void loop(String endpoint) { + int sizeBytes = 16; + getResponse(endpoint, sizeBytes) + .statusCode(500) + .body(CoreMatchers.containsString( + "Redirect loop detected on Conduit '{https://quarkiverse.github.io/quarkiverse-docs/quarkus-cxf/test}LargeSlowServicePort.http-conduit'")); + } + + static ValidatableResponse getResponse(String endpoint, int sizeBytes) { + if (endpoint.startsWith("async")) { + /* URLConnectionHTTPConduitFactory does not support async */ + Assumptions.assumeThat(HTTPConduitImpl.findDefaultHTTPConduitImpl()) + .isNotEqualTo(HTTPConduitImpl.URLConnectionHTTPConduitFactory); + } + + return RestAssured.given() + .queryParam("sizeBytes", String.valueOf(sizeBytes)) + .queryParam("delayMs", "0") + .get("/RedirectRest/" + endpoint) + .then(); + } + +}