WIP start moving FhirRequestBuilder to ManagedWebAccess

This commit is contained in:
dotasek 2024-10-21 17:19:53 -04:00
parent 9dc08c213c
commit 435dbbec9b
10 changed files with 293 additions and 95 deletions

View File

@ -50,7 +50,6 @@ import org.hl7.fhir.r5.model.ResourceFactory;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.DebugUtilities;
import org.hl7.fhir.utilities.Utilities;
/**

View File

@ -35,7 +35,6 @@ import org.hl7.fhir.r5.model.ElementDefinition;
import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.DebugUtilities;
public class PEDefinitionElement extends PEDefinition {

View File

@ -2,7 +2,6 @@ package org.hl7.fhir.r5.terminologies.client;
import java.net.URISyntaxException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/*
@ -41,13 +40,10 @@ import org.hl7.fhir.r5.model.CanonicalResource;
import org.hl7.fhir.r5.model.CapabilityStatement;
import org.hl7.fhir.r5.model.CodeSystem;
import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.TerminologyCapabilities;
import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.terminologies.client.ITerminologyClient;
import org.hl7.fhir.r5.terminologies.client.TerminologyClientManager.ITerminologyClientFactory;
import org.hl7.fhir.r5.terminologies.client.TerminologyClientR5.TerminologyClientR5Factory;
import org.hl7.fhir.r5.utils.client.FHIRToolingClient;
import org.hl7.fhir.r5.utils.client.network.ClientHeaders;
import org.hl7.fhir.utilities.FhirPublication;
@ -84,6 +80,7 @@ public class TerminologyClientR5 implements ITerminologyClient {
public TerminologyClientR5(String id, String address, String userAgent) throws URISyntaxException {
this.client = new FHIRToolingClient(address, userAgent);
//FIXME set up FHIR Tooling Client to use ManagedWebAccess
setClientHeaders(new ClientHeaders());
this.id = id;
}

View File

@ -7,8 +7,8 @@ import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import okhttp3.*;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.formats.IParser;
import org.hl7.fhir.r5.formats.JsonParser;
import org.hl7.fhir.r5.formats.XmlParser;
@ -21,16 +21,10 @@ import org.hl7.fhir.r5.utils.client.EFhirClientException;
import org.hl7.fhir.r5.utils.client.ResourceFormat;
import org.hl7.fhir.utilities.MimeType;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.settings.FhirSettings;
import org.hl7.fhir.utilities.http.FhirRequest;
import org.hl7.fhir.utilities.http.ManagedWebAccess;
import org.hl7.fhir.utilities.xhtml.XhtmlUtils;
import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class FhirRequestBuilder {
protected static final String HTTP_PROXY_USER = "http.proxyUser";
@ -63,6 +57,16 @@ public class FhirRequestBuilder {
private FhirLoggingInterceptor logger = null;
private String source;
//TODO this should be the only constructor. There should be no okHttp exposure.
public FhirRequestBuilder(FhirRequest fhirRequest, String source) {
this.source = source;
RequestBody body = RequestBody.create(fhirRequest.getBody());
this.httpRequest = new Request.Builder()
.url(fhirRequest.getUrl())
.method(fhirRequest.getMethod().name(), body);
}
public FhirRequestBuilder(Request.Builder httpRequest, String source) {
this.httpRequest = httpRequest;
this.source = source;
@ -162,11 +166,9 @@ public class FhirRequestBuilder {
*
* @return {@link OkHttpClient} instance
*/
//TODO replace this.
protected OkHttpClient getHttpClient() {
if (FhirSettings.isProhibitNetworkAccess()) {
throw new FHIRException("Network Access is prohibited in this context");
}
if (okHttpClient == null) {
okHttpClient = new OkHttpClient();
}
@ -235,7 +237,7 @@ public class FhirRequestBuilder {
public <T extends Resource> ResourceRequest<T> execute() throws IOException {
formatHeaders(httpRequest, resourceFormat, headers);
Response response = getHttpClient().newCall(httpRequest.build()).execute();
Response response = ManagedWebAccess.httpCall(httpRequest);//getHttpClient().newCall(httpRequest.build()).execute();
T resource = unmarshalReference(response, resourceFormat, null);
return new ResourceRequest<T>(resource, response.code(), getLocationHeader(response.headers()));
}

View File

@ -0,0 +1,22 @@
package org.hl7.fhir.utilities.http;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.With;
@AllArgsConstructor
public class FhirRequest {
public enum HttpMethod {
GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH
}
@With @Getter
private final String url;
@With @Getter
private HttpMethod method;
@With @Getter
private byte[] body;
}

View File

@ -0,0 +1,73 @@
package org.hl7.fhir.utilities.http;
import lombok.With;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class ManagedFhirWebAccessBuilder extends ManagedWebAccessBuilderBase<ManagedFhirWebAccessBuilder>{
/**
* The singleton instance of the HttpClient, used for all requests.
*/
private static OkHttpClient okHttpClient;
private long timeout;
private int retries;
public ManagedFhirWebAccessBuilder withTimeout(long timeout) {
this.timeout = timeout;
return this;
}
public ManagedFhirWebAccessBuilder withRetries(int retries) {
this.retries = retries;
return this;
}
public ManagedFhirWebAccessBuilder(String userAgent, List<ServerDetailsPOJO> serverAuthDetails) {
super(userAgent, serverAuthDetails);
}
private void setHeaders(Request.Builder httpRequest) {
for (Map.Entry<String, String> entry : this.getHeaders().entrySet()) {
httpRequest.header(entry.getKey(), entry.getValue());
}
}
public Response httpCall(Request.Builder httpRequest) throws IOException {
switch (ManagedWebAccess.getAccessPolicy()) {
case DIRECT:
OkHttpClient okHttpClient = getOkHttpClient();
//TODO check and throw based on httpRequest:
// if (!ManagedWebAccess.inAllowedPaths(url)) {
// throw new IOException("The pathname '"+url+"' cannot be accessed by policy");
// }
//TODO add auth headers to httpRequest
return okHttpClient.newCall(httpRequest.build()).execute();
case MANAGED:
setHeaders(httpRequest);
return ManagedWebAccess.getFhirWebAccessor().httpCall(httpRequest);
case PROHIBITED:
throw new IOException("Access to the internet is not allowed by local security policy");
default:
throw new IOException("Internal Error");
}
}
private OkHttpClient getOkHttpClient() {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient();
}
return okHttpClient;
}
}

View File

@ -42,6 +42,9 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import okhttp3.Request;
import okhttp3.Response;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
@ -57,13 +60,17 @@ import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
*
*/
public class ManagedWebAccess {
public interface IWebAccessor {
HTTPResult get(String url, String accept, Map<String, String> headers) throws IOException;
HTTPResult post(String url, byte[] bytes, String contentType, String accept, Map<String, String> headers) throws IOException;
HTTPResult put(String url, byte[] bytes, String contentType, String accept, Map<String, String> headers) throws IOException;
}
public interface IFhirWebAccessor {
Response httpCall(Request.Builder httpRequest);
}
public enum WebAccessPolicy {
DIRECT, // open access to the web, though access can be restricted only to domains in AllowedDomains
MANAGED, // no access except by the IWebAccessor
@ -72,11 +79,16 @@ public class ManagedWebAccess {
private static WebAccessPolicy accessPolicy = WebAccessPolicy.DIRECT; // for legacy reasons
private static List<String> allowedDomains = new ArrayList<>();
@Getter
private static IWebAccessor accessor;
@Getter
private static IFhirWebAccessor fhirWebAccessor;
@Getter
private static String userAgent;
private static List<ServerDetailsPOJO> serverAuthDetails;
public static WebAccessPolicy getAccessPolicy() {
return accessPolicy;
}
@ -97,22 +109,18 @@ public class ManagedWebAccess {
return false;
}
public static String getUserAgent() {
return userAgent;
}
public static void setUserAgent(String userAgent) {
ManagedWebAccess.userAgent = userAgent;
}
public static IWebAccessor getAccessor() {
return accessor;
}
public static ManagedWebAccessBuilder builder() {
return new ManagedWebAccessBuilder(userAgent, serverAuthDetails);
}
public static ManagedFhirWebAccessBuilder fhirBuilder() {
return new ManagedFhirWebAccessBuilder(userAgent, serverAuthDetails);
}
public static HTTPResult get(String url) throws IOException {
return builder().get(url);
}
@ -130,4 +138,8 @@ public class ManagedWebAccess {
return builder().withAccept(accept).put(url, content, contentType);
}
public static Response httpCall(Request.Builder httpRequest) throws IOException {
return fhirBuilder().httpCall(httpRequest);
}
}

View File

@ -7,61 +7,37 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
/**
* Simple HTTP client for making requests to a server.
*/
public class ManagedWebAccessBuilder extends ManagedWebAccessBuilderBase<ManagedWebAccessBuilder> {
public class ManagedWebAccessBuilder {
private String userAgent;
private HTTPAuthenticationMode authenticationMode;
private String username;
private String password;
private String token;
private String accept;
private List<ServerDetailsPOJO> serverAuthDetails;
private Map<String, String> headers = new HashMap<String, String>();
public ManagedWebAccessBuilder(String userAgent, List<ServerDetailsPOJO> serverAuthDetails) {
this.userAgent = userAgent;
this.serverAuthDetails = serverAuthDetails;
super(userAgent, serverAuthDetails);
}
public ManagedWebAccessBuilder withAccept(String accept) {
this.accept = accept;
return this;
}
public ManagedWebAccessBuilder withHeader(String name, String value) {
headers.put(name, value);
return this;
}
public ManagedWebAccessBuilder withBasicAuth(String username, String password) {
this.authenticationMode = HTTPAuthenticationMode.BASIC;
this.username = username;
this.password = password;
return this;
return super.withAccept(accept);
}
public ManagedWebAccessBuilder withToken(String token) {
this.authenticationMode = HTTPAuthenticationMode.TOKEN;
this.token = token;
return this;
}
private Map<String, String> headers() {
private Map<String, String> newHeaders() {
Map<String, String> headers = new HashMap<String, String>();
headers.putAll(this.headers);
if (authenticationMode == HTTPAuthenticationMode.TOKEN) {
headers.put("Authorization", "Bearer " + token);
} else if (authenticationMode == HTTPAuthenticationMode.BASIC) {
String auth = username+":"+password;
headers.putAll(this.getHeaders());
if (getAuthenticationMode() == HTTPAuthenticationMode.TOKEN) {
headers.put("Authorization", "Bearer " + getToken());
} else if (getAuthenticationMode() == HTTPAuthenticationMode.BASIC) {
String auth = getUsername() +":"+ getPassword();
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(StandardCharsets.UTF_8));
headers.put("Authorization", "Basic " + new String(encodedAuth));
}
if (userAgent != null) {
headers.put("User-Agent", userAgent);
if (getUserAgent() != null) {
headers.put("User-Agent", getUserAgent());
}
return headers;
@ -72,21 +48,21 @@ public class ManagedWebAccessBuilder {
throw new IOException("The pathname '"+url+"' cannot be accessed by policy");
}
SimpleHTTPClient client = new SimpleHTTPClient();
if (userAgent != null) {
client.addHeader("User-Agent", userAgent);
if (getUserAgent() != null) {
client.addHeader("User-Agent", getUserAgent());
}
if (authenticationMode != null && authenticationMode != HTTPAuthenticationMode.NONE) {
client.setAuthenticationMode(authenticationMode);
switch (authenticationMode) {
if (getAuthenticationMode() != null && getAuthenticationMode() != HTTPAuthenticationMode.NONE) {
client.setAuthenticationMode(getAuthenticationMode());
switch (getAuthenticationMode()) {
case BASIC :
client.setUsername(username);
client.setPassword(password);
client.setUsername(getUsername());
client.setPassword(getPassword());
break;
case TOKEN :
client.setToken(token);
client.setToken(getToken());
break;
case APIKEY :
client.setApiKey(token);
client.setApiKey(getToken());
break;
}
} else {
@ -109,17 +85,15 @@ public class ManagedWebAccessBuilder {
}
}
}
if (username != null || token != null) {
client.setAuthenticationMode(authenticationMode);
if (getUsername() != null || getToken() != null) {
client.setAuthenticationMode(getAuthenticationMode());
}
return client;
}
private ServerDetailsPOJO getServer(String url) {
if (serverAuthDetails != null) {
for (ServerDetailsPOJO t : serverAuthDetails) {
if (getServerAuthDetails() != null) {
for (ServerDetailsPOJO t : getServerAuthDetails()) {
if (url.startsWith(t.getUrl())) {
return t;
}
@ -132,9 +106,9 @@ public class ManagedWebAccessBuilder {
switch (ManagedWebAccess.getAccessPolicy()) {
case DIRECT:
SimpleHTTPClient client = setupClient(url);
return client.get(url, accept);
return client.get(url, getAccept());
case MANAGED:
return ManagedWebAccess.getAccessor().get(url, accept, headers());
return ManagedWebAccess.getAccessor().get(url, getAccept(), newHeaders());
case PROHIBITED:
throw new IOException("Access to the internet is not allowed by local security policy");
default:
@ -142,14 +116,13 @@ public class ManagedWebAccessBuilder {
}
}
public HTTPResult post(String url, byte[] content, String contentType) throws IOException {
switch (ManagedWebAccess.getAccessPolicy()) {
case DIRECT:
SimpleHTTPClient client = setupClient(url);
return client.post(url, contentType, content, accept);
return client.post(url, contentType, content, getAccept());
case MANAGED:
return ManagedWebAccess.getAccessor().post(url, content, contentType, accept, headers());
return ManagedWebAccess.getAccessor().post(url, content, contentType, getAccept(), newHeaders());
case PROHIBITED:
throw new IOException("Access to the internet is not allowed by local security policy");
default:
@ -161,14 +134,13 @@ public class ManagedWebAccessBuilder {
switch (ManagedWebAccess.getAccessPolicy()) {
case DIRECT:
SimpleHTTPClient client = setupClient(url);
return client.put(url, contentType, content, accept);
return client.put(url, contentType, content, getAccept());
case MANAGED:
return ManagedWebAccess.getAccessor().put(url, content, contentType, accept, headers());
return ManagedWebAccess.getAccessor().put(url, content, contentType, getAccept(), newHeaders());
case PROHIBITED:
throw new IOException("Access to the internet is not allowed by local security policy");
default:
throw new IOException("Internal Error");
}
}
}

View File

@ -0,0 +1,60 @@
package org.hl7.fhir.utilities.http;
import lombok.Getter;
import org.hl7.fhir.utilities.settings.ServerDetailsPOJO;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public abstract class ManagedWebAccessBuilderBase<B extends ManagedWebAccessBuilderBase<B>> {
@Getter
private final String userAgent;
@Getter
private HTTPAuthenticationMode authenticationMode;
@Getter
private String username;
@Getter
private String password;
@Getter
private String token;
@Getter
private String accept;
@Getter
private final List<ServerDetailsPOJO> serverAuthDetails;
@Getter
private Map<String, String> headers = new HashMap<String, String>();
public ManagedWebAccessBuilderBase(String userAgent, List<ServerDetailsPOJO> serverAuthDetails) {
this.userAgent = userAgent;
this.serverAuthDetails = serverAuthDetails;
}
@SuppressWarnings("unchecked")
final B self() {
return (B) this;
}
public B withAccept(String accept) {
this.accept = accept;
return self();
}
public B withHeader(String name, String value) {
headers.put(name, value);
return self();
}
public B withBasicAuth(String username, String password) {
this.authenticationMode = HTTPAuthenticationMode.BASIC;
this.username = username;
this.password = password;
return self();
}
public B withToken(String token) {
this.authenticationMode = HTTPAuthenticationMode.TOKEN;
this.token = token;
return self();
}
}

View File

@ -0,0 +1,62 @@
package org.hl7.fhir.utilities.http.okhttpimpl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
/**
* An {@link Interceptor} for {@link okhttp3.OkHttpClient} that controls the number of times we retry a to execute a
* given request, before reporting a failure. This includes unsuccessful return codes and timeouts.
*/
public class RetryInterceptor implements Interceptor {
// Delay between retying failed requests, in millis
private final long RETRY_TIME = 2000;
// Maximum number of times to retry the request before failing
private final int maxRetry;
// Internal counter for tracking the number of times we've tried this request
private int retryCounter = 0;
public RetryInterceptor(int maxRetry) {
this.maxRetry = maxRetry;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
do {
try {
// If we are retrying a failed request that failed due to a bad response from the server, we must close it first
if (response != null) {
// System.out.println("Previous " + chain.request().method() + " attempt returned HTTP<" + (response.code())
// + "> from url -> " + chain.request().url() + ".");
response.close();
}
// System.out.println(chain.request().method() + " attempt <" + (retryCounter + 1) + "> to url -> " + chain.request().url());
response = chain.proceed(request);
} catch (IOException e) {
try {
// Include a small break in between requests.
Thread.sleep(RETRY_TIME);
} catch (InterruptedException e1) {
System.out.println(chain.request().method() + " to url -> " + chain.request().url() + " interrupted on try <" + retryCounter + ">");
}
} finally {
retryCounter++;
}
} while ((response == null || !response.isSuccessful()) && (retryCounter <= maxRetry + 1));
/*
* if something has gone wrong, and we are unable to complete the request, we still need to initialize the return
* response so we don't get a null pointer exception.
*/
return response != null ? response : chain.proceed(request);
}
}