Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#4289 - Ability to skip SSL certificate validation on external recommenders #4290

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,24 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.io.IOUtils;
import org.apache.uima.cas.CAS;
import org.apache.uima.cas.CASException;
Expand Down Expand Up @@ -80,14 +89,15 @@ public class ExternalRecommender
{
public static final Key<Boolean> KEY_TRAINING_COMPLETE = new Key<>("training_complete");

private static final Logger LOG = LoggerFactory.getLogger(ExternalRecommender.class);
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

private static final int HTTP_TOO_MANY_REQUESTS = 429;
private static final int HTTP_BAD_REQUEST = 400;

private final ExternalRecommenderProperties properties;
private final ExternalRecommenderTraits traits;
private final HttpClient client;

private HttpClient _client;

public ExternalRecommender(ExternalRecommenderProperties aProperties, Recommender aRecommender,
ExternalRecommenderTraits aTraits)
Expand All @@ -96,7 +106,56 @@ public ExternalRecommender(ExternalRecommenderProperties aProperties, Recommende

properties = aProperties;
traits = aTraits;
client = HttpClient.newBuilder().connectTimeout(properties.getConnectTimeout()).build();
}

private HttpClient getClient() throws RecommendationException
{
try {
if (_client == null) {
var clientBuilder = HttpClient.newBuilder() //
.connectTimeout(properties.getConnectTimeout());
if (!traits.isVerifyCertificates()) {
var sslContext = makeNonVerifyingSslContext();

clientBuilder.sslContext(sslContext);

}
_client = clientBuilder.build();
}
return _client;
}
catch (KeyManagementException | NoSuchAlgorithmException e) {
throw new RecommendationException("Unable to initialize HTTP client", e);
}
}

private SSLContext makeNonVerifyingSslContext()
throws NoSuchAlgorithmException, KeyManagementException
{
var trustManager = new X509TrustManager()
{
@Override
public X509Certificate[] getAcceptedIssuers()
{
return null;
}

@Override
public void checkClientTrusted(X509Certificate[] certs, String authType)
{
// no check
}

@Override
public void checkServerTrusted(X509Certificate[] certs, String authType)
{
// no check
}
};

var sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom());
return sslContext;
}

@Override
Expand All @@ -113,10 +172,12 @@ public boolean isReadyForPrediction(RecommenderContext aContext)
@Override
public void train(RecommenderContext aContext, List<CAS> aCasses) throws RecommendationException
{
TrainingRequest trainingRequest = new TrainingRequest();
var client = getClient();

var trainingRequest = new TrainingRequest();

// We assume that the type system for all CAS are the same
String typeSystem = serializeTypeSystem(aCasses.get(0));
var typeSystem = serializeTypeSystem(aCasses.get(0));
trainingRequest.setTypeSystem(typeSystem);

// Fill in metadata. We use the type system of the first CAS in the list
Expand All @@ -128,19 +189,19 @@ public void train(RecommenderContext aContext, List<CAS> aCasses) throws Recomme

trainingRequest.setMetadata(buildMetadata(aCasses.get(0)));

List<Document> documents = new ArrayList<>();
for (CAS cas : aCasses) {
var documents = new ArrayList<Document>();
for (var cas : aCasses) {
documents.add(buildDocument(cas));
}
trainingRequest.setDocuments(documents);

HttpRequest request = HttpRequest.newBuilder() //
var request = HttpRequest.newBuilder() //
.uri(URI.create(appendIfMissing(traits.getRemoteUrl(), "/")).resolve("train")) //
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE) //
.timeout(properties.getReadTimeout())
.POST(BodyPublishers.ofString(toJson(trainingRequest), UTF_8)).build();

HttpResponse<String> response = sendRequest(request);
HttpResponse<String> response = sendRequest(client, request);
if (response.statusCode() == HTTP_TOO_MANY_REQUESTS) {
LOG.info("External recommender is already training");
}
Expand All @@ -161,23 +222,25 @@ else if (response.statusCode() >= HTTP_BAD_REQUEST) {
public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd)
throws RecommendationException
{
String typeSystem = serializeTypeSystem(aCas);
var client = getClient();

var typeSystem = serializeTypeSystem(aCas);

PredictionRequest predictionRequest = new PredictionRequest();
var predictionRequest = new PredictionRequest();
predictionRequest.setTypeSystem(typeSystem);
predictionRequest.setDocument(buildDocument(aCas));

// Fill in metadata
predictionRequest.setMetadata(buildMetadata(aCas));

HttpRequest request = HttpRequest.newBuilder() //
var request = HttpRequest.newBuilder() //
.uri(URI.create(appendIfMissing(traits.getRemoteUrl(), "/")).resolve("predict")) //
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE) //
.timeout(properties.getReadTimeout()) //
.POST(BodyPublishers.ofString(toJson(predictionRequest), UTF_8)) //
.build();

HttpResponse<String> response = sendRequest(request);
HttpResponse<String> response = sendRequest(client, request);
// If the response indicates that the request was not successful,
// then it does not make sense to go on and try to decode the XMI
if (response.statusCode() >= HTTP_BAD_REQUEST) {
Expand Down Expand Up @@ -288,10 +351,11 @@ private String toJson(Object aObject) throws RecommendationException
}
}

private HttpResponse<String> sendRequest(HttpRequest aRequest) throws RecommendationException
private HttpResponse<String> sendRequest(HttpClient aClient, HttpRequest aRequest)
throws RecommendationException
{
try {
return client.send(aRequest, BodyHandlers.ofString(UTF_8));
return aClient.send(aRequest, BodyHandlers.ofString(UTF_8));
}
catch (IOException | InterruptedException e) {
throw new RecommendationException("Error while sending request: " + e.getMessage(), e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class ExternalRecommenderTraits

private String remoteUrl;
private boolean trainable;
private boolean verifyCertificates = true;

public String getRemoteUrl()
{
Expand All @@ -49,4 +50,14 @@ public void setTrainable(boolean aTrainable)
{
trainable = aTrainable;
}

public void setVerifyCertificates(boolean aVerifyCertificates)
{
verifyCertificates = aVerifyCertificates;
}

public boolean isVerifyCertificates()
{
return verifyCertificates;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@
<input wicket:id="remoteUrl" type="text" class="form-control"></input>
</div>
</div>
<div class="row form-row" wicket:enclosure="verifyCertificates">
<div class="offset-sm-3 col-sm-9">
<div class="form-check">
<input wicket:id="verifyCertificates" class="form-check-input" type="checkbox"/>
<label wicket:for="verifyCertificates" class="form-check-label">
<wicket:label key="verifyCertificates"/>
</label>
</div>
</div>
</div>
<div class="row form-row" wicket:enclosure="trainable">
<div class="offset-sm-3 col-sm-9">
<div class="form-check">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,16 @@ protected void onSubmit()
}
};

TextField<String> remoteUrl = new TextField<>("remoteUrl");
var remoteUrl = new TextField<String>("remoteUrl");
remoteUrl.setRequired(true);
remoteUrl.add(new UrlValidator());
form.add(remoteUrl);

CheckBox trainable = new CheckBox("trainable");
var verifyCertificates = new CheckBox("verifyCertificates");
verifyCertificates.setOutputMarkupId(true);
form.add(verifyCertificates);

var trainable = new CheckBox("trainable");
trainable.setOutputMarkupId(true);
trainable.add(new LambdaAjaxFormComponentUpdatingBehavior("change",
_target -> _target.add(getTrainingStatesChoice())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@

remoteUrl=Remote URL
trainable=Trainable
verifyCertificates=Verify certificates