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 00000000000..c169ab68278 Binary files /dev/null and b/hapi-fhir-client-apache-http5/src/test/resources/client-keystore.p12 differ 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 00000000000..d12c7199708 Binary files /dev/null and b/hapi-fhir-client-apache-http5/src/test/resources/client-truststore.p12 differ 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 00000000000..ccce89d0aba Binary files /dev/null and b/hapi-fhir-client-apache-http5/src/test/resources/keystore.jks differ 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 00000000000..00ae45348ef Binary files /dev/null and b/hapi-fhir-client-apache-http5/src/test/resources/server-keystore.p12 differ diff --git a/hapi-fhir-client-apache-http5/src/test/resources/server-truststore.p12 b/hapi-fhir-client-apache-http5/src/test/resources/server-truststore.p12 new file mode 100644 index 00000000000..65575b455b6 Binary files /dev/null and b/hapi-fhir-client-apache-http5/src/test/resources/server-truststore.p12 differ diff --git a/hapi-fhir-client-apache-http5/src/test/resources/truststore.jks b/hapi-fhir-client-apache-http5/src/test/resources/truststore.jks new file mode 100644 index 00000000000..f4fa4000747 Binary files /dev/null and b/hapi-fhir-client-apache-http5/src/test/resources/truststore.jks differ 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