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.
This commit is contained in:
parent
b7e94ece95
commit
43ce89cda6
|
@ -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
|
||||
|
|
|
@ -44,6 +44,11 @@
|
|||
<artifactId>hapi-fhir-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>hapi-fhir-client-apache-http5</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>hapi-fhir-client-okhttp</artifactId>
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>7.7.7-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-client-apache-http5</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>HAPI FHIR - Client Framework using Apache HttpClient 5</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-base</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<groupId>commons-logging</groupId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpcore</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache HTTP Client 5 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<groupId>commons-logging</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.core5</groupId>
|
||||
<artifactId>httpcore5</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* <p/>
|
||||
* 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<String, List<String>> getAllHeaders() {
|
||||
return myOrigHttpResponse.getAllHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, List<String>> getAllHeaders() {
|
||||
Map<String, List<String>> 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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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<String, List<String>> getAllHeaders() {
|
||||
Map<String, List<String>> headers = new HashMap<>();
|
||||
Header[] allHeaders = myResponse.getHeaders();
|
||||
if (allHeaders != null) {
|
||||
for (Header next : allHeaders) {
|
||||
String name = next.getName().toLowerCase();
|
||||
List<String> list = headers.computeIfAbsent(name, k -> new ArrayList<>());
|
||||
list.add(next.getValue());
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getHeaders(String theName) {
|
||||
Header[] headers = myResponse.getHeaders(theName);
|
||||
if (headers == null) {
|
||||
headers = new Header[0];
|
||||
}
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, List<String>> theIfNoneExistParams,
|
||||
String theIfNoneExistString,
|
||||
RequestTypeEnum theRequestType,
|
||||
List<Header> 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<NameValuePair> 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<String, List<String>> theParams) {
|
||||
List<NameValuePair> parameters = new ArrayList<>();
|
||||
for (Entry<String, List<String>> nextParam : theParams.entrySet()) {
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
|
@ -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<String, List<String>> theIfNoneExistParams,
|
||||
String theIfNoneExistString,
|
||||
RequestTypeEnum theRequestType,
|
||||
List<Header> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<TrustStoreInfo> 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<TrustStoreInfo> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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> trustStoreInfo = Optional.empty();
|
||||
HostnameVerifier result = ApacheHttp5TlsAuthenticationSvc.createHostnameVerifier(trustStoreInfo);
|
||||
assertEquals(NoopHostnameVerifier.class, result.getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateHostnameVerifierPresentTrustStoreInfo(){
|
||||
Optional<TrustStoreInfo> trustStoreInfo = Optional.of(myServerTrustStoreInfo);
|
||||
HostnameVerifier result = ApacheHttp5TlsAuthenticationSvc.createHostnameVerifier(trustStoreInfo);
|
||||
assertEquals(DefaultHostnameVerifier.class, result.getClass());
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -41,6 +41,11 @@
|
|||
<artifactId>hapi-fhir-client</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-client-apache-http5</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-client-okhttp</artifactId>
|
||||
|
|
|
@ -162,6 +162,11 @@
|
|||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-client-apache-http5</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-client-okhttp</artifactId>
|
||||
|
|
|
@ -53,6 +53,12 @@
|
|||
<version>${project.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-client-apache-http5</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-client-okhttp</artifactId>
|
||||
|
|
14
pom.xml
14
pom.xml
|
@ -115,6 +115,7 @@
|
|||
<module>hapi-fhir-jpaserver-mdm</module>
|
||||
<module>hapi-fhir-testpage-overlay</module>
|
||||
<module>hapi-fhir-jpaserver-uhnfhirtest</module>
|
||||
<module>hapi-fhir-client-apache-http5</module>
|
||||
<module>hapi-fhir-client-okhttp</module>
|
||||
<module>hapi-fhir-android</module>
|
||||
<module>hapi-fhir-cli</module>
|
||||
|
@ -125,6 +126,7 @@
|
|||
<module>hapi-fhir-spring-boot</module>
|
||||
<module>hapi-fhir-jacoco</module>
|
||||
<module>hapi-fhir-server-cds-hooks</module>
|
||||
|
||||
</modules>
|
||||
|
||||
<dependencies>
|
||||
|
@ -1029,6 +1031,8 @@
|
|||
<hibernate_validator_version>8.0.0.Final</hibernate_validator_version>
|
||||
<httpcore_version>4.4.13</httpcore_version>
|
||||
<httpclient_version>4.5.13</httpclient_version>
|
||||
<httpclient5_version>5.4.1</httpclient5_version>
|
||||
<httpcore5_version>5.3.1</httpcore5_version>
|
||||
<jackson_version>2.17.1</jackson_version>
|
||||
<jackson_databind_version>2.17.1</jackson_databind_version>
|
||||
<maven_assembly_plugin_version>3.3.0</maven_assembly_plugin_version>
|
||||
|
@ -1560,6 +1564,16 @@
|
|||
<artifactId>commons-text</artifactId>
|
||||
<version>${commons_text_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.core5</groupId>
|
||||
<artifactId>httpcore5</artifactId>
|
||||
<version>${httpcore5_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>${httpclient5_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
|
|
Loading…
Reference in New Issue