From 43ce89cda614a682200e5c5c48b39b7dbf717385 Mon Sep 17 00:00:00 2001 From: Ibrahim Tallouzi <110388796+iyt-trifork@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:46:18 +0100 Subject: [PATCH] Introduce hapi-fhir-client-apache-http5 module for Apache HttpClient 5 support (#6520) * feat: Introduce hapi-fhir-client-apache-http5 module for Apache HttpClient 5 support - Added a new module `hapi-fhir-client-apache-http5` to provide HAPI FHIR Client functionality using Apache HttpClient 5. - Supports gradual migration from HttpClient 4 to HttpClient 5. - Aligns with Spring Boot 3.0's adoption of HttpClient 5, enabling consistent HTTP client configuration for users of both libraries. Key Changes: - Integrated Apache HttpClient 5 for modern, high-performance HTTP requests. - Ensured compatibility with existing `hapi-fhir-client` and `hapi-fhir-client-okhttp` modules. - Added basic tests to validate functionality and coexistence of HttpClient 4 and 5. Impact: - Non-breaking change; the new module can be adopted independently. - Facilitates eventual migration of HAPI FHIR to HttpClient 5 across the codebase." * Add new error codes to the apache-httpclient5 client module. --- azure-pipelines.yml | 2 + hapi-fhir-bom/pom.xml | 5 + hapi-fhir-client-apache-http5/pom.xml | 60 ++++++ .../ApacheHttp5GZipContentInterceptor.java | 74 ++++++++ .../ApacheHttp5ModifiedStringResponse.java | 135 +++++++++++++ .../client/apache/ApacheHttp5Request.java | 131 +++++++++++++ .../apache/ApacheHttp5ResourceEntity.java | 42 +++++ .../client/apache/ApacheHttp5Response.java | 177 ++++++++++++++++++ .../apache/ApacheHttp5RestfulClient.java | 142 ++++++++++++++ .../ApacheHttp5RestfulClientFactory.java | 170 +++++++++++++++++ .../tls/ApacheHttp5TlsAuthenticationSvc.java | 148 +++++++++++++++ .../client/apache/ApacheHttp5RequestTest.java | 40 ++++ .../ApacheHttp5TlsAuthenticationSvcTest.java | 158 ++++++++++++++++ .../src/test/resources/client-keystore.p12 | Bin 0 -> 4424 bytes .../src/test/resources/client-truststore.p12 | Bin 0 -> 1782 bytes .../src/test/resources/keystore.jks | Bin 0 -> 3945 bytes .../src/test/resources/server-keystore.p12 | Bin 0 -> 4408 bytes .../src/test/resources/server-truststore.p12 | Bin 0 -> 1814 bytes .../src/test/resources/truststore.jks | Bin 0 -> 1472 bytes hapi-fhir-docs/pom.xml | 5 + hapi-fhir-jacoco/pom.xml | 5 + .../pom.xml | 6 + pom.xml | 14 ++ 23 files changed, 1314 insertions(+) create mode 100644 hapi-fhir-client-apache-http5/pom.xml create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5GZipContentInterceptor.java create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ModifiedStringResponse.java create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Request.java create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ResourceEntity.java create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Response.java create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClient.java create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClientFactory.java create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvc.java create mode 100644 hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RequestTest.java create mode 100644 hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvcTest.java create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/client-keystore.p12 create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/client-truststore.p12 create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/keystore.jks create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/server-keystore.p12 create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/server-truststore.p12 create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/truststore.jks diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5756e093c49..7e89540de50 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -44,6 +44,8 @@ stages: module: hapi-fhir-cli/hapi-fhir-cli-api - name: hapi_fhir_client module: hapi-fhir-client + - name: hapi_fhir_client_apache_http5 + module: hapi-fhir-client-apache-http5 - name: hapi_fhir_client_okhttp module: hapi-fhir-client-okhttp - name: hapi_fhir_converter diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index 08f577f3dff..9269f6860ef 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -44,6 +44,11 @@ hapi-fhir-client ${project.version} + + ${project.groupId} + hapi-fhir-client-apache-http5 + ${project.version} + ${project.groupId} hapi-fhir-client-okhttp diff --git a/hapi-fhir-client-apache-http5/pom.xml b/hapi-fhir-client-apache-http5/pom.xml new file mode 100644 index 00000000000..00c91cb4f2b --- /dev/null +++ b/hapi-fhir-client-apache-http5/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + ca.uhn.hapi.fhir + hapi-deployable-pom + 7.7.7-SNAPSHOT + + ../hapi-deployable-pom/pom.xml + + + hapi-fhir-client-apache-http5 + jar + + HAPI FHIR - Client Framework using Apache HttpClient 5 + + + + ca.uhn.hapi.fhir + hapi-fhir-base + ${project.version} + + + commons-logging + commons-logging + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + + + + + org.apache.httpcomponents.client5 + httpclient5 + + + commons-logging + commons-logging + + + + + org.apache.httpcomponents.core5 + httpcore5 + + + + ca.uhn.hapi.fhir + hapi-fhir-client + ${project.version} + + + diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5GZipContentInterceptor.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5GZipContentInterceptor.java new file mode 100644 index 00000000000..8a5e8ed9a4f --- /dev/null +++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5GZipContentInterceptor.java @@ -0,0 +1,74 @@ +/* + * #%L + * HAPI FHIR - Client Framework + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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. + * #L% + */ +package ca.uhn.fhir.rest.client.apache; + +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.client.api.IClientInterceptor; +import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.client.api.IHttpResponse; +import org.apache.hc.client5.http.classic.methods.HttpUriRequest; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.GZIPOutputStream; + +/** + * Client interceptor which GZip compresses outgoing (POST/PUT) contents being uploaded + * from the client to the server. This can improve performance by reducing network + * load time. + */ +public class ApacheHttp5GZipContentInterceptor implements IClientInterceptor { + private static final org.slf4j.Logger ourLog = + org.slf4j.LoggerFactory.getLogger(ApacheHttp5GZipContentInterceptor.class); + + @Override + public void interceptRequest(IHttpRequest theRequestInterface) { + HttpUriRequest theRequest = ((ApacheHttp5Request) theRequestInterface).getApacheRequest(); + if (theRequest != null) { + Header[] encodingHeaders = theRequest.getHeaders(Constants.HEADER_CONTENT_ENCODING); + if (encodingHeaders == null || encodingHeaders.length == 0) { + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + GZIPOutputStream gos; + try { + gos = new GZIPOutputStream(bos); + theRequest.getEntity().writeTo(gos); + gos.finish(); + } catch (IOException e) { + ourLog.warn("Failed to GZip outgoing content", e); + return; + } + + byte[] byteArray = bos.toByteArray(); + ByteArrayEntity newEntity = new ByteArrayEntity(byteArray, ContentType.APPLICATION_OCTET_STREAM); + theRequest.setEntity(newEntity); + theRequest.addHeader(Constants.HEADER_CONTENT_ENCODING, "gzip"); + } + } + } + + @Override + public void interceptResponse(IHttpResponse theResponse) throws IOException { + // nothing + } +} diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ModifiedStringResponse.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ModifiedStringResponse.java new file mode 100644 index 00000000000..cb11c80ca12 --- /dev/null +++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ModifiedStringResponse.java @@ -0,0 +1,135 @@ +/* + * #%L + * HAPI FHIR - Client Framework + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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. + * #L% + */ +package ca.uhn.fhir.rest.client.apache; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.client.api.IHttpResponse; +import ca.uhn.fhir.rest.client.impl.BaseHttpResponse; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.StopWatch; +import org.apache.commons.io.IOUtils; + +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +/** + * Process a modified copy of an existing {@link IHttpResponse} with a String containing new content. + *

+ * Meant to be used with custom interceptors that need to hijack an existing IHttpResponse with new content. + */ +public class ApacheHttp5ModifiedStringResponse extends BaseHttpResponse implements IHttpResponse { + private static final org.slf4j.Logger ourLog = + org.slf4j.LoggerFactory.getLogger(ApacheHttp5ModifiedStringResponse.class); + private boolean myEntityBuffered = false; + private final String myNewContent; + private final IHttpResponse myOrigHttpResponse; + private byte[] myEntityBytes = null; + + public ApacheHttp5ModifiedStringResponse( + IHttpResponse theOrigHttpResponse, String theNewContent, StopWatch theResponseStopWatch) { + super(theResponseStopWatch); + myOrigHttpResponse = theOrigHttpResponse; + myNewContent = theNewContent; + } + + @Override + public void bufferEntity() throws IOException { + if (myEntityBuffered) { + return; + } + try (InputStream respEntity = readEntity()) { + if (respEntity != null) { + try { + myEntityBytes = IOUtils.toByteArray(respEntity); + } catch (IllegalStateException exception) { + throw new InternalErrorException(Msg.code(2580) + exception); + } + myEntityBuffered = true; + } + } + } + + @Override + public void close() { + if (myOrigHttpResponse instanceof Closeable) { + try { + ((Closeable) myOrigHttpResponse).close(); + } catch (IOException exception) { + ourLog.debug("Failed to close response", exception); + } + } + } + + @Override + public Reader createReader() { + return new InputStreamReader(readEntity(), StandardCharsets.UTF_8); + } + + @Override + public Map> getAllHeaders() { + return myOrigHttpResponse.getAllHeaders(); + } + + @Override + public List getHeaders(String theName) { + return myOrigHttpResponse.getHeaders(theName); + } + + @Override + public String getMimeType() { + return myOrigHttpResponse.getMimeType(); + } + + @Override + public StopWatch getRequestStopWatch() { + return myOrigHttpResponse.getRequestStopWatch(); + } + + @Override + public Object getResponse() { + return null; + } + + @Override + public int getStatus() { + return myOrigHttpResponse.getStatus(); + } + + @Override + public String getStatusInfo() { + return myOrigHttpResponse.getStatusInfo(); + } + + @Override + public InputStream readEntity() { + if (myEntityBuffered) { + return new ByteArrayInputStream(myEntityBytes); + } else { + return new ByteArrayInputStream(myNewContent.getBytes()); + } + } +} diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Request.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Request.java new file mode 100644 index 00000000000..a70be6c4c21 --- /dev/null +++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Request.java @@ -0,0 +1,131 @@ +/* + * #%L + * HAPI FHIR - Client Framework + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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. + * #L% + */ +package ca.uhn.fhir.rest.client.apache; + +import ca.uhn.fhir.rest.client.api.BaseHttpRequest; +import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.client.api.IHttpResponse; +import ca.uhn.fhir.util.StopWatch; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.classic.methods.HttpUriRequest; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * A Http Request based on Apache. This is an adapter around the class + * {@link HttpUriRequest HttpRequestBase } + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +public class ApacheHttp5Request extends BaseHttpRequest implements IHttpRequest { + + private HttpClient myClient; + private HttpUriRequest myRequest; + + public ApacheHttp5Request(HttpClient theClient, HttpUriRequest theApacheRequest) { + this.myClient = theClient; + this.myRequest = theApacheRequest; + } + + @Override + public void addHeader(String theName, String theValue) { + myRequest.addHeader(theName, theValue); + } + + @Override + public IHttpResponse execute() throws IOException { + StopWatch responseStopWatch = new StopWatch(); + return myClient.execute(myRequest, httpResponse -> new ApacheHttp5Response(httpResponse, responseStopWatch)); + } + + @Override + public Map> getAllHeaders() { + Map> result = new HashMap<>(); + for (Header header : myRequest.getHeaders()) { + if (!result.containsKey(header.getName())) { + result.put(header.getName(), new LinkedList<>()); + } + result.get(header.getName()).add(header.getValue()); + } + return Collections.unmodifiableMap(result); + } + + /** + * Get the ApacheRequest + * + * @return the ApacheRequest + */ + public HttpUriRequest getApacheRequest() { + return myRequest; + } + + @Override + public String getHttpVerbName() { + return myRequest.getMethod(); + } + + @Override + public void removeHeaders(String theHeaderName) { + Validate.notBlank(theHeaderName, "theHeaderName must not be null or blank"); + myRequest.removeHeaders(theHeaderName); + } + + @Override + public String getRequestBodyFromStream() throws IOException { + if (myRequest != null) { + HttpEntity entity = myRequest.getEntity(); + if (entity.isRepeatable()) { + final Header contentTypeHeader = myRequest.getFirstHeader("Content-Type"); + Charset charset = contentTypeHeader == null + ? null + : ContentType.parse(contentTypeHeader.getValue()).getCharset(); + return IOUtils.toString(entity.getContent(), charset); + } + } + return null; + } + + @Override + public String getUri() { + return myRequest.getRequestUri().toString(); + } + + @Override + public void setUri(String theUrl) { + myRequest.setUri(URI.create(theUrl)); + } + + @Override + public String toString() { + return myRequest.toString(); + } +} diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ResourceEntity.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ResourceEntity.java new file mode 100644 index 00000000000..a0e97607654 --- /dev/null +++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ResourceEntity.java @@ -0,0 +1,42 @@ +/*- + * #%L + * HAPI FHIR - Client Framework + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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. + * #L% + */ +package ca.uhn.fhir.rest.client.apache; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.Constants; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.nio.charset.UnsupportedCharsetException; + +/** + * Apache HttpClient request content entity where the body is a FHIR resource, that will + * be encoded as JSON by default + */ +public class ApacheHttp5ResourceEntity extends StringEntity { + + public ApacheHttp5ResourceEntity(FhirContext theContext, IBaseResource theResource) + throws UnsupportedCharsetException { + super( + theContext.newJsonParser().encodeResourceToString(theResource), + ContentType.parse(Constants.CT_FHIR_JSON_NEW)); + } +} diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Response.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Response.java new file mode 100644 index 00000000000..a9b7aefabf1 --- /dev/null +++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Response.java @@ -0,0 +1,177 @@ +/* + * #%L + * HAPI FHIR - Client Framework + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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. + * #L% + */ +package ca.uhn.fhir.rest.client.apache; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.client.api.IHttpResponse; +import ca.uhn.fhir.rest.client.impl.BaseHttpResponse; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.StopWatch; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpResponse; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A Http Response based on Apache. This is an adapter around the class + * {@link org.apache.hc.core5.http.ClassicHttpResponse HttpResponse} + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +public class ApacheHttp5Response extends BaseHttpResponse implements IHttpResponse { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ApacheHttp5Response.class); + + private boolean myEntityBuffered = false; + private byte[] myEntityBytes; + private final ClassicHttpResponse myResponse; + + public ApacheHttp5Response(ClassicHttpResponse theResponse, StopWatch theResponseStopWatch) { + super(theResponseStopWatch); + this.myResponse = theResponse; + } + + @Override + public void bufferEntity() throws IOException { + if (myEntityBuffered) { + return; + } + try (InputStream respEntity = readEntity()) { + if (respEntity != null) { + this.myEntityBuffered = true; + try { + this.myEntityBytes = IOUtils.toByteArray(respEntity); + } catch (IllegalStateException e) { + throw new InternalErrorException(Msg.code(2581) + e); + } + } + } + } + + @Override + public void close() { + if (myResponse instanceof CloseableHttpResponse) { + try { + myResponse.close(); + } catch (IOException e) { + ourLog.debug("Failed to close response", e); + } + } + } + + @Override + public Reader createReader() throws IOException { + HttpEntity entity = myResponse.getEntity(); + if (entity == null) { + return new StringReader(""); + } + Charset charset = null; + String contentType = entity.getContentType(); + if (StringUtils.isNotBlank(contentType)) { + ContentType ct = ContentType.parse(contentType); + charset = ct.getCharset(); + } + if (charset == null) { + if (Constants.STATUS_HTTP_204_NO_CONTENT != myResponse.getCode()) { + ourLog.debug("Response did not specify a charset, defaulting to utf-8"); + } + charset = StandardCharsets.UTF_8; + } + + return new InputStreamReader(readEntity(), charset); + } + + @Override + public Map> getAllHeaders() { + Map> headers = new HashMap<>(); + Header[] allHeaders = myResponse.getHeaders(); + if (allHeaders != null) { + for (Header next : allHeaders) { + String name = next.getName().toLowerCase(); + List list = headers.computeIfAbsent(name, k -> new ArrayList<>()); + list.add(next.getValue()); + } + } + return headers; + } + + @Override + public List getHeaders(String theName) { + Header[] headers = myResponse.getHeaders(theName); + if (headers == null) { + headers = new Header[0]; + } + List retVal = new ArrayList<>(); + for (Header next : headers) { + retVal.add(next.getValue()); + } + return retVal; + } + + @Override + public String getMimeType() { + ContentType ct = ContentType.parse(myResponse.getEntity().getContentType()); + return ct != null ? ct.getMimeType() : null; + } + + @Override + public HttpResponse getResponse() { + return myResponse; + } + + @Override + public int getStatus() { + return myResponse.getCode(); + } + + @Override + public String getStatusInfo() { + return myResponse.getReasonPhrase(); + } + + @Override + public InputStream readEntity() throws IOException { + if (this.myEntityBuffered) { + return new ByteArrayInputStream(myEntityBytes); + } else if (myResponse.getEntity() != null) { + return myResponse.getEntity().getContent(); + } else { + return null; + } + } +} diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClient.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClient.java new file mode 100644 index 00000000000..d7ecd6f761c --- /dev/null +++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClient.java @@ -0,0 +1,142 @@ +/* + * #%L + * HAPI FHIR - Client Framework + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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. + * #L% + */ +package ca.uhn.fhir.rest.client.apache; + +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.client.api.Header; +import ca.uhn.fhir.rest.client.api.IHttpClient; +import ca.uhn.fhir.rest.client.api.IHttpRequest; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.classic.methods.*; +import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.message.BasicNameValuePair; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static org.apache.hc.core5.http.ContentType.APPLICATION_OCTET_STREAM; + +/** + * A Http Client based on Apache. This is an adapter around the class + * {@link org.apache.hc.client5.http.classic HttpClient} + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +public class ApacheHttp5RestfulClient extends BaseHttpClient implements IHttpClient { + + private final HttpClient myClient; + private final HttpHost host; + + public ApacheHttp5RestfulClient( + HttpClient theClient, + StringBuilder theUrl, + Map> theIfNoneExistParams, + String theIfNoneExistString, + RequestTypeEnum theRequestType, + List

theHeaders) { + super(theUrl, theIfNoneExistParams, theIfNoneExistString, theRequestType, theHeaders); + this.myClient = theClient; + this.host = new HttpHost(theUrl.toString()); + } + + private HttpUriRequestBase constructRequestBase(HttpEntity theEntity) { + String url = myUrl.toString(); + switch (myRequestType) { + case DELETE: + return new HttpDelete(url); + case PATCH: + HttpPatch httpPatch = new HttpPatch(url); + httpPatch.setEntity(theEntity); + return httpPatch; + case OPTIONS: + return new HttpOptions(url); + case POST: + HttpPost httpPost = new HttpPost(url); + httpPost.setEntity(theEntity); + return httpPost; + case PUT: + HttpPut httpPut = new HttpPut(url); + httpPut.setEntity(theEntity); + return httpPut; + case GET: + default: + return new HttpGet(url); + } + } + + private UrlEncodedFormEntity createFormEntity(List parameters) { + return new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8); + } + + @Override + protected IHttpRequest createHttpRequest() { + return createHttpRequest((HttpEntity) null); + } + + @Override + protected IHttpRequest createHttpRequest(byte[] content) { + /* + * Note: Be careful about changing which constructor we use for + * ByteArrayEntity, as Android's version of HTTPClient doesn't support + * the newer ones for whatever reason. + */ + ByteArrayEntity entity = new ByteArrayEntity(content, APPLICATION_OCTET_STREAM); + return createHttpRequest(entity); + } + + private ApacheHttp5Request createHttpRequest(HttpEntity theEntity) { + HttpUriRequest request = constructRequestBase(theEntity); + return new ApacheHttp5Request(myClient, request); + } + + @Override + protected IHttpRequest createHttpRequest(Map> theParams) { + List parameters = new ArrayList<>(); + for (Entry> nextParam : theParams.entrySet()) { + List value = nextParam.getValue(); + for (String s : value) { + parameters.add(new BasicNameValuePair(nextParam.getKey(), s)); + } + } + + UrlEncodedFormEntity entity = createFormEntity(parameters); + return createHttpRequest(entity); + } + + @Override + protected IHttpRequest createHttpRequest(String theContents) { + /* + * We aren't using a StringEntity here because the constructors + * supported by Android aren't available in non-Android, and vice versa. + * Since we add the content type header manually, it makes no difference + * which one we use anyhow. + */ + ByteArrayEntity entity = + new ByteArrayEntity(theContents.getBytes(StandardCharsets.UTF_8), APPLICATION_OCTET_STREAM); + return createHttpRequest(entity); + } +} diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClientFactory.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClientFactory.java new file mode 100644 index 00000000000..979d0910169 --- /dev/null +++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClientFactory.java @@ -0,0 +1,170 @@ +/* + * #%L + * HAPI FHIR - Client Framework + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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. + * #L% + */ +package ca.uhn.fhir.rest.client.apache; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.client.api.Header; +import ca.uhn.fhir.rest.client.api.IHttpClient; +import ca.uhn.fhir.rest.client.impl.RestfulClientFactory; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.util.TimeValue; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +/** + * A Restful Factory to create clients, requests and responses based on the Apache httpclient library. + * + * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare + */ +public class ApacheHttp5RestfulClientFactory extends RestfulClientFactory { + + private HttpClient myHttpClient; + private HttpHost myProxy; + + /** + * Constructor + */ + public ApacheHttp5RestfulClientFactory() { + super(); + } + + /** + * Constructor + * + * @param theContext + * The context + */ + public ApacheHttp5RestfulClientFactory(FhirContext theContext) { + super(theContext); + } + + @Override + protected synchronized IHttpClient getHttpClient(String theServerBase) { + return getHttpClient(new StringBuilder(theServerBase), null, null, null, null); + } + + @Override + public synchronized IHttpClient getHttpClient( + StringBuilder theUrl, + Map> theIfNoneExistParams, + String theIfNoneExistString, + RequestTypeEnum theRequestType, + List
theHeaders) { + return new ApacheHttp5RestfulClient( + getNativeHttpClient(), theUrl, theIfNoneExistParams, theIfNoneExistString, theRequestType, theHeaders); + } + + public HttpClient getNativeHttpClient() { + if (myHttpClient == null) { + ConnectionConfig connectionConfig = ConnectionConfig.custom() + .setConnectTimeout(getConnectTimeout(), TimeUnit.MILLISECONDS) + .build(); + + RequestConfig defaultRequestConfig = RequestConfig.custom() + .setResponseTimeout(getSocketTimeout(), TimeUnit.MILLISECONDS) + .setConnectionRequestTimeout(getConnectionRequestTimeout(), TimeUnit.MILLISECONDS) + .build(); + + SocketConfig socketConfig = SocketConfig.custom() + .setSoTimeout(getSocketTimeout(), TimeUnit.MILLISECONDS) + .build(); + + HttpClientBuilder builder = getHttpClientBuilder() + .useSystemProperties() + .setDefaultRequestConfig(defaultRequestConfig) + .disableCookieManagement(); + + PoolingHttpClientConnectionManager connectionManager = + createPoolingHttpClientConnectionManager(socketConfig, connectionConfig); + builder.setConnectionManager(connectionManager); + + if (myProxy != null && isNotBlank(getProxyUsername()) && isNotBlank(getProxyPassword())) { + BasicCredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials( + new AuthScope(myProxy.getHostName(), myProxy.getPort()), + new UsernamePasswordCredentials( + getProxyUsername(), getProxyPassword().toCharArray())); + builder.setProxyAuthenticationStrategy(new DefaultAuthenticationStrategy()); + builder.setDefaultCredentialsProvider(credsProvider); + } + + myHttpClient = builder.build(); + } + + return myHttpClient; + } + + private PoolingHttpClientConnectionManager createPoolingHttpClientConnectionManager( + SocketConfig socketConfig, ConnectionConfig connectionConfig) { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(getPoolMaxTotal()); + connectionManager.setDefaultMaxPerRoute(getPoolMaxPerRoute()); + connectionManager.setDefaultSocketConfig(socketConfig); + connectionManager.setDefaultConnectionConfig(connectionConfig); + connectionManager.setConnectionConfigResolver(route -> ConnectionConfig.custom() + .setValidateAfterInactivity( + TimeValue.ofSeconds(5)) // Validate connections after 5 seconds of inactivity + .build()); + return connectionManager; + } + + protected HttpClientBuilder getHttpClientBuilder() { + return HttpClients.custom(); + } + + @Override + protected void resetHttpClient() { + this.myHttpClient = null; + } + + /** + * Only allows to set an instance of type org.apache.hc.client5.http.classic.HttpClient + * @see ca.uhn.fhir.rest.client.api.IRestfulClientFactory#setHttpClient(Object) + */ + @Override + public synchronized void setHttpClient(Object theHttpClient) { + this.myHttpClient = (HttpClient) theHttpClient; + } + + @Override + public void setProxy(String theHost, Integer thePort) { + if (theHost != null) { + myProxy = new HttpHost("http", theHost, thePort); + } else { + myProxy = null; + } + } +} diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvc.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvc.java new file mode 100644 index 00000000000..b791f8dd122 --- /dev/null +++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvc.java @@ -0,0 +1,148 @@ +/*- + * #%L + * HAPI FHIR - Client Framework + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * 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. + * #L% + */ +package ca.uhn.fhir.rest.client.tls; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.tls.BaseStoreInfo; +import ca.uhn.fhir.tls.KeyStoreInfo; +import ca.uhn.fhir.tls.PathType; +import ca.uhn.fhir.tls.TlsAuthentication; +import ca.uhn.fhir.tls.TrustStoreInfo; +import jakarta.annotation.Nonnull; +import org.apache.commons.lang3.Validate; +import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; +import org.apache.hc.core5.ssl.PrivateKeyStrategy; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.hc.core5.ssl.SSLContexts; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyStore; +import java.util.Optional; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +public class ApacheHttp5TlsAuthenticationSvc { + + private ApacheHttp5TlsAuthenticationSvc() {} + + public static SSLContext createSslContext(@Nonnull TlsAuthentication theTlsAuthentication) { + Validate.notNull(theTlsAuthentication, "theTlsAuthentication cannot be null"); + + try { + SSLContextBuilder contextBuilder = SSLContexts.custom(); + + if (theTlsAuthentication.getKeyStoreInfo().isPresent()) { + KeyStoreInfo keyStoreInfo = + theTlsAuthentication.getKeyStoreInfo().get(); + PrivateKeyStrategy privateKeyStrategy = null; + if (isNotBlank(keyStoreInfo.getAlias())) { + privateKeyStrategy = (aliases, socket) -> keyStoreInfo.getAlias(); + } + KeyStore keyStore = createKeyStore(keyStoreInfo); + contextBuilder.loadKeyMaterial(keyStore, keyStoreInfo.getKeyPass(), privateKeyStrategy); + } + + if (theTlsAuthentication.getTrustStoreInfo().isPresent()) { + TrustStoreInfo trustStoreInfo = + theTlsAuthentication.getTrustStoreInfo().get(); + KeyStore trustStore = createKeyStore(trustStoreInfo); + contextBuilder.loadTrustMaterial(trustStore, TrustSelfSignedStrategy.INSTANCE); + } + + return contextBuilder.build(); + } catch (Exception e) { + throw new TlsAuthenticationException(Msg.code(2575) + "Failed to create SSLContext", e); + } + } + + public static KeyStore createKeyStore(BaseStoreInfo theStoreInfo) { + try { + KeyStore keyStore = KeyStore.getInstance(theStoreInfo.getType().toString()); + + if (PathType.RESOURCE.equals(theStoreInfo.getPathType())) { + try (InputStream inputStream = + ApacheHttp5TlsAuthenticationSvc.class.getResourceAsStream(theStoreInfo.getFilePath())) { + validateKeyStoreExists(inputStream); + keyStore.load(inputStream, theStoreInfo.getStorePass()); + } + } else if (PathType.FILE.equals(theStoreInfo.getPathType())) { + try (InputStream inputStream = new FileInputStream(theStoreInfo.getFilePath())) { + validateKeyStoreExists(inputStream); + keyStore.load(inputStream, theStoreInfo.getStorePass()); + } + } + return keyStore; + } catch (Exception e) { + throw new TlsAuthenticationException(Msg.code(2576) + "Failed to create KeyStore", e); + } + } + + public static void validateKeyStoreExists(InputStream theInputStream) { + if (theInputStream == null) { + throw new TlsAuthenticationException(Msg.code(2577) + "Keystore does not exists"); + } + } + + public static X509TrustManager createTrustManager(Optional theTrustStoreInfo) { + try { + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + if (!theTrustStoreInfo.isPresent()) { + trustManagerFactory.init((KeyStore) null); // Load Trust Manager Factory with default Java truststore + } else { + TrustStoreInfo trustStoreInfo = theTrustStoreInfo.get(); + KeyStore trustStore = createKeyStore(trustStoreInfo); + trustManagerFactory.init(trustStore); + } + for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) { + if (trustManager instanceof X509TrustManager) { + return (X509TrustManager) trustManager; + } + } + throw new TlsAuthenticationException(Msg.code(2578) + "Could not find X509TrustManager"); + } catch (Exception e) { + throw new TlsAuthenticationException(Msg.code(2579) + "Failed to create X509TrustManager"); + } + } + + public static HostnameVerifier createHostnameVerifier(Optional theTrustStoreInfo) { + return theTrustStoreInfo.isPresent() ? new DefaultHostnameVerifier() : new NoopHostnameVerifier(); + } + + public static class TlsAuthenticationException extends RuntimeException { + private static final long serialVersionUID = 1l; + + public TlsAuthenticationException(String theMessage, Throwable theCause) { + super(theMessage, theCause); + } + + public TlsAuthenticationException(String theMessage) { + super(theMessage); + } + } +} diff --git a/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RequestTest.java b/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RequestTest.java new file mode 100644 index 00000000000..0ea69159bad --- /dev/null +++ b/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RequestTest.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.rest.client.apache; + +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ApacheHttp5RequestTest { + + private final String ENTITY_CONTENT = "Some entity with special characters: é"; + private StringEntity entity; + private final HttpPost request = new HttpPost(""); + + @Test + public void testGetRequestBodyFromStream() throws IOException { + entity = new StringEntity(ENTITY_CONTENT, StandardCharsets.ISO_8859_1); + request.setHeader("Content-type", "text/plain; charset=ISO-8859-1"); + request.setEntity(entity); + + String result = new ApacheHttp5Request(null, request).getRequestBodyFromStream(); + + assertEquals(ENTITY_CONTENT, result); + } + + @Test + public void testGetRequestBodyFromStreamWithDefaultCharset() throws IOException { + entity = new StringEntity(ENTITY_CONTENT, Charset.defaultCharset()); + request.setHeader("Content-type", "text/plain"); + request.setEntity(entity); + + String result = new ApacheHttp5Request(null, request).getRequestBodyFromStream(); + + assertEquals(ENTITY_CONTENT, result); + } +} diff --git a/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvcTest.java b/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvcTest.java new file mode 100644 index 00000000000..42caea25cbf --- /dev/null +++ b/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvcTest.java @@ -0,0 +1,158 @@ +package ca.uhn.fhir.rest.client.tls; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.tls.KeyStoreInfo; +import ca.uhn.fhir.tls.TlsAuthentication; +import ca.uhn.fhir.tls.TrustStoreInfo; +import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + + +public class ApacheHttp5TlsAuthenticationSvcTest { + + private KeyStoreInfo myServerKeyStoreInfo; + private TrustStoreInfo myServerTrustStoreInfo; + private TlsAuthentication myServerTlsAuthentication; + + private KeyStoreInfo myClientKeyStoreInfo; + private TrustStoreInfo myClientTrustStoreInfo; + private TlsAuthentication myClientTlsAuthentication; + + @BeforeEach + public void beforeEach(){ + myServerKeyStoreInfo = new KeyStoreInfo("classpath:/server-keystore.p12", "changeit", "changeit", "server"); + myServerTrustStoreInfo = new TrustStoreInfo("classpath:/server-truststore.p12", "changeit", "client"); + myServerTlsAuthentication = new TlsAuthentication(Optional.of(myServerKeyStoreInfo), Optional.of(myServerTrustStoreInfo)); + + myClientKeyStoreInfo = new KeyStoreInfo("classpath:/client-keystore.p12", "changeit", "changeit", "client"); + myClientTrustStoreInfo = new TrustStoreInfo("classpath:/client-truststore.p12", "changeit", "server"); + myClientTlsAuthentication = new TlsAuthentication(Optional.of(myClientKeyStoreInfo), Optional.of(myClientTrustStoreInfo)); + } + + @Test + public void testCreateSslContextEmpty(){ + TlsAuthentication emptyAuthentication = null; + try { + ApacheHttp5TlsAuthenticationSvc.createSslContext(emptyAuthentication); + fail(); } catch (Exception e) { + assertEquals("theTlsAuthentication cannot be null", e.getMessage()); + } + } + + @Test + public void testCreateSslContextPresent(){ + SSLContext result = ApacheHttp5TlsAuthenticationSvc.createSslContext(myServerTlsAuthentication); + assertEquals("TLS", result.getProtocol()); + } + + @Test + public void testCreateSslContextPresentInvalid(){ + KeyStoreInfo invalidKeyStoreInfo = new KeyStoreInfo("file:///INVALID.p12", "changeit", "changeit", "server"); + TlsAuthentication invalidTlsAuthentication = new TlsAuthentication(Optional.of(invalidKeyStoreInfo), Optional.of(myServerTrustStoreInfo)); + try { + ApacheHttp5TlsAuthenticationSvc.createSslContext(invalidTlsAuthentication); + fail(); } catch (Exception e) { + assertEquals(Msg.code(2575) + "Failed to create SSLContext", e.getMessage()); + } + } + + @Test + public void testCreateKeyStoreP12() throws Exception { + KeyStore keyStore = ApacheHttp5TlsAuthenticationSvc.createKeyStore(myServerKeyStoreInfo); + assertNotNull(keyStore.getKey(myServerKeyStoreInfo.getAlias(), myServerKeyStoreInfo.getKeyPass())); + } + + @Test + public void testCreateKeyStoreJKS() throws Exception { + KeyStoreInfo keyStoreInfo = new KeyStoreInfo("classpath:/keystore.jks", "changeit", "changeit", "client"); + KeyStore keyStore = ApacheHttp5TlsAuthenticationSvc.createKeyStore(keyStoreInfo); + assertNotNull(keyStore.getKey(keyStoreInfo.getAlias(), keyStoreInfo.getKeyPass())); + } + + @Test + public void testCreateKeyStoreNonExistentFile() { + KeyStoreInfo keyStoreInfo = new KeyStoreInfo("classpath:/non-existent.p12", "changeit", "changeit", "server"); + try { + ApacheHttp5TlsAuthenticationSvc.createKeyStore(keyStoreInfo); + fail(); } + catch (Exception e) { + assertEquals(Msg.code(2576) + "Failed to create KeyStore", e.getMessage()); + } + } + + @Test + public void testCreateTrustStoreJks() throws Exception { + TrustStoreInfo trustStoreInfo = new TrustStoreInfo("classpath:/truststore.jks", "changeit", "client"); + KeyStore keyStore = ApacheHttp5TlsAuthenticationSvc.createKeyStore(trustStoreInfo); + assertNotNull(keyStore.getCertificate(trustStoreInfo.getAlias())); + } + + @Test + public void testCreateTrustStoreP12() throws Exception { + KeyStore keyStore = ApacheHttp5TlsAuthenticationSvc.createKeyStore(myServerTrustStoreInfo); + assertNotNull(keyStore.getCertificate(myServerTrustStoreInfo.getAlias())); + } + + @Test + public void testCreateTrustStoreNonExistentFile() throws Exception { + TrustStoreInfo trustStoreInfo = new TrustStoreInfo("classpath:/non-existent.p12", "changeit", "server"); + try { + ApacheHttp5TlsAuthenticationSvc.createKeyStore(trustStoreInfo); + fail(); } catch (Exception e) { + assertEquals(Msg.code(2576) + "Failed to create KeyStore", e.getMessage()); + } + } + + @Test + public void testCreateTrustManager() throws Exception { + X509TrustManager trustManager = ApacheHttp5TlsAuthenticationSvc.createTrustManager(Optional.of(myClientTrustStoreInfo)); + KeyStore keyStore = ApacheHttp5TlsAuthenticationSvc.createKeyStore(myServerKeyStoreInfo); + Certificate serverCertificate = keyStore.getCertificate(myServerKeyStoreInfo.getAlias()); + + assertEquals(1, trustManager.getAcceptedIssuers().length); + assertEquals(serverCertificate, trustManager.getAcceptedIssuers()[0]); + } + + @Test + public void testCreateTrustManagerNoTrustStore() { + // trust manager should contain common certifications if no trust store information is used + X509TrustManager trustManager = ApacheHttp5TlsAuthenticationSvc.createTrustManager(Optional.empty()); + assertThat(trustManager.getAcceptedIssuers().length).isNotEqualTo(0); + } + + @Test + public void testCreateTrustManagerInvalid() { + TrustStoreInfo invalidKeyStoreInfo = new TrustStoreInfo("file:///INVALID.p12", "changeit", "client"); + try { + ApacheHttp5TlsAuthenticationSvc.createTrustManager(Optional.of(invalidKeyStoreInfo)); + fail(); } catch (Exception e) { + assertEquals(Msg.code(2579) + "Failed to create X509TrustManager", e.getMessage()); + } + } + + @Test + public void testCreateHostnameVerifierEmptyTrustStoreInfo(){ + Optional trustStoreInfo = Optional.empty(); + HostnameVerifier result = ApacheHttp5TlsAuthenticationSvc.createHostnameVerifier(trustStoreInfo); + assertEquals(NoopHostnameVerifier.class, result.getClass()); + } + + @Test + public void testCreateHostnameVerifierPresentTrustStoreInfo(){ + Optional trustStoreInfo = Optional.of(myServerTrustStoreInfo); + HostnameVerifier result = ApacheHttp5TlsAuthenticationSvc.createHostnameVerifier(trustStoreInfo); + assertEquals(DefaultHostnameVerifier.class, result.getClass()); + } +} diff --git a/hapi-fhir-client-apache-http5/src/test/resources/client-keystore.p12 b/hapi-fhir-client-apache-http5/src/test/resources/client-keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..c169ab6827861698f9e14d981f02396ef8fc886a GIT binary patch literal 4424 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 literal 0 HcmV?d00001 diff --git a/hapi-fhir-client-apache-http5/src/test/resources/client-truststore.p12 b/hapi-fhir-client-apache-http5/src/test/resources/client-truststore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..d12c71997081e84eb7c77cb38c09dba50c5b368c GIT binary patch literal 1782 zcmV7Duzgg_YDCD0ic2gjRb-Qi72`Yw2hW8Bt2LUi<1_>&LNQUZFWRng#81`;k7TD8Xxi8sx1NnClCSwATSID2r7n1hW8Bu2?YQ! z9R>+thDZTr0|Wso1Q2gUT%)B6<&<*E{DV-sW+Q-t1@I1SdjZl^feORkdVb)XH|}vs zkrvif2K0tLng^@hcztwxT9v^TXG&ZxtJ6@l=$t0tT z<#eE8nJrwU3T|!GU)a`H0#%bBI{`qxMr=71F0iMv!2)(igtI^1kdS&1CW3g)Ridzr z#FP|k%MJPNu@@kNU`Gq^_nX8ZD+?sY{2<=K!p2Wn6L+3+*_P2_eO0SBus-6qFJUK* z#8FPsYz<5sEXAd9=oLM#L0W;O4^=ex(;^WFfa-jD&_#r>$mHK^9Z#3%NGFIKYB%1w zfHj9alf4C!9fi87SgSPfbl(Z?I}=@nV@Gt$K37d(XdD}ylvDu((P*FONWnzWrIZP;cdXZ+j=esNkPK^HLdRQ8WR z+ocJyRl>!6TzLbZ=zdePwQyy!QhQ=t6}U-F?(AmL9Z2%ns-mi}H0q}(8`x~s>bt#d zEcJb{24_?QPj)1Q5=YiEtpXw(L~U^A-U^r5H#cJ%GNV8}n=%{1V=BnQ+Ptb)I_UJ_ zEDi)>wAExeyhOv^7ScWL+cDjXLJ(ZeEwfY&{t$GlN@y~$PkQaH{;aoxRRF4+{+K;bDQrO zEMfv!pR@jM6>UKle+JG|jJk9AHBYPem3K={({jTsGq*^TS2P2&ta{&|?S9vh!J3tv|uAmoze zaary(QpW1b*pd!>TdPQpVyj_-vg%zLS3R3^s z6W53cLyT>f#1oC8hS4QuCF0_UIx+PybWhc z;pB&FLZ%rYguqx^rKoinA}98nmTEb&ks_dXMS$fOFD|#dhx}5B53>~AhD8w*K$xqb zO)={7!A)XLm_>hgiI4k)y4XZ@6!PgT7H3Sv8h@JdzoT3$^MFU{rs3Y088)b=MK z`eP(PF=%7HT~6?eBPhu;rLSb5Zn<&6csmmb#r`=|m5&imAW1!qB1PExvw6r+e=+PT_q)}uJT(~y{W*mK z(6_@ZAu43< z>fbi0d*qZNONC9O71OfpC00bbzO{+$R-JkQjd;;qHMZ%g^Dvxjijp>&N)e+pbjh8G06z#3y Ylixyj7^TM)2`lKFV|9Ym<^lpI5JOi(6951J literal 0 HcmV?d00001 diff --git a/hapi-fhir-client-apache-http5/src/test/resources/keystore.jks b/hapi-fhir-client-apache-http5/src/test/resources/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..ccce89d0aba8bca41d13ef811a710943334c920d GIT binary patch literal 3945 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 literal 0 HcmV?d00001 diff --git a/hapi-fhir-client-apache-http5/src/test/resources/server-keystore.p12 b/hapi-fhir-client-apache-http5/src/test/resources/server-keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..00ae45348ef293d40db10cc642ca9796abf0d700 GIT binary patch literal 4408 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 V`iZW$UsV;_m}amDFRzrm0RS^bS#JOU literal 0 HcmV?d00001 diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 79b067aafd4..0a55640077f 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -41,6 +41,11 @@ hapi-fhir-client ${project.version} + + ca.uhn.hapi.fhir + hapi-fhir-client-apache-http5 + ${project.version} + ca.uhn.hapi.fhir hapi-fhir-client-okhttp diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 86e3fd4383b..dac274bae96 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -162,6 +162,11 @@ ${project.version} --> + + ca.uhn.hapi.fhir + hapi-fhir-client-apache-http5 + ${project.version} + ca.uhn.hapi.fhir hapi-fhir-client-okhttp diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml index 5f2a71be410..49c7e0eea55 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -53,6 +53,12 @@ ${project.version} true + + ca.uhn.hapi.fhir + hapi-fhir-client-apache-http5 + ${project.version} + true + ca.uhn.hapi.fhir hapi-fhir-client-okhttp diff --git a/pom.xml b/pom.xml index 1cba1096550..82e20acaf66 100644 --- a/pom.xml +++ b/pom.xml @@ -115,6 +115,7 @@ hapi-fhir-jpaserver-mdm hapi-fhir-testpage-overlay hapi-fhir-jpaserver-uhnfhirtest + hapi-fhir-client-apache-http5 hapi-fhir-client-okhttp hapi-fhir-android hapi-fhir-cli @@ -125,6 +126,7 @@ hapi-fhir-spring-boot hapi-fhir-jacoco hapi-fhir-server-cds-hooks + @@ -1029,6 +1031,8 @@ 8.0.0.Final 4.4.13 4.5.13 + 5.4.1 + 5.3.1 2.17.1 2.17.1 3.3.0 @@ -1560,6 +1564,16 @@ commons-text ${commons_text_version} + + org.apache.httpcomponents.core5 + httpcore5 + ${httpcore5_version} + + + org.apache.httpcomponents.client5 + httpclient5 + ${httpclient5_version} + org.apache.httpcomponents httpclient