
uk.num.numlib.api.NumAPIImpl Maven / Gradle / Ivy
/*
* Copyright (c) 2019. NUM Technology Ltd
*/
package uk.num.numlib.api;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xbill.DNS.ExtendedResolver;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record;
import org.xbill.DNS.SimpleResolver;
import uk.num.numlib.exc.*;
import uk.num.numlib.internal.ctx.AppContext;
import uk.num.numlib.internal.ctx.NumAPIContextBase;
import uk.num.numlib.internal.dns.DNSServices;
import uk.num.numlib.internal.dns.DNSServicesDefaultImpl;
import uk.num.numlib.internal.dns.PossibleMultiPartRecordException;
import uk.num.numlib.internal.modl.ModlServices;
import uk.num.numlib.internal.modl.NumLookupRedirect;
import uk.num.numlib.internal.modl.NumQueryRedirect;
import uk.num.numlib.internal.modl.PopulatorResponse;
import uk.num.numlib.internal.module.Module;
import uk.num.numlib.internal.module.ModuleConfig;
import uk.num.numlib.internal.module.ModuleDNSQueries;
import uk.num.numlib.internal.module.ModuleFactory;
import uk.num.numlib.internal.util.EncryptionUtils;
import uk.num.numlib.internal.util.PopulatorRetryConfig;
import java.io.IOException;
import java.net.UnknownHostException;
import java.security.Key;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static uk.num.numlib.api.NumAPICallbacks.Location.*;
/**
* This is the main class for using the num-client-library.
* Use the default constructor to use the DNS servers configured on your local machine,
* or override these by supplying a specific DNS host domain name using the alternative constructor.
*
* @author tonywalmsley
*/
public class NumAPIImpl implements NumAPI {
private static final Logger LOG = LoggerFactory.getLogger(NumAPIImpl.class);
private static final int MAX_NUMBER_OF_MULTI_PARTS = 30;
/**
* The ApplicationContext to use for this NUM API Session
*/
private final AppContext appContext = new AppContext();
private final ModuleFactory moduleFactory = new ModuleFactory();
/**
* Services for accessing DNS and processing the resulting records.
*/
private DNSServices dnsServices;
/**
* Services for running the MODL Interpreter
*/
private ModlServices modlServices;
/**
* Supports running DNS queries asynchronously.
*/
private ExecutorService executor = Executors.newSingleThreadExecutor();
/**
* Default constructor to initialise the default DNS services and MODL services.
*/
public NumAPIImpl() {
LOG.info("enter - NumAPI()");
dnsServices = new DNSServicesDefaultImpl();
modlServices = new ModlServices();
LOG.info("NumAPI object created.");
}
/**
* Constructor allowing the core services to be overridden, mainly for testing purposes, but could also be useful for other purposes.
*
* @param dnsServices Replacement DNSServices implementation.
* @param modlServices Replacement ModlServices implementation.
*/
public NumAPIImpl(final DNSServices dnsServices, final ModlServices modlServices) {
LOG.info("enter - NumAPI(dnsServices, modlServices)");
this.dnsServices = dnsServices;
this.modlServices = modlServices;
LOG.info("NumAPI object created.");
}
/**
* Alternative constructor used to override the default DNS hosts. Unit tests rely on this constructor.
*
* @param dnsHost The DNS host to override the defaults configured for the local machine.
* @throws NumInvalidDNSHostException on error
*/
public NumAPIImpl(final String dnsHost) throws NumInvalidDNSHostException {
this();
LOG.info("enter - NumAPI({})", dnsHost);
try {
if (!StringUtils.isEmpty(dnsHost)) {
final SimpleResolver resolver = new SimpleResolver(dnsHost);
Lookup.setDefaultResolver(resolver);
}
} catch (UnknownHostException e) {
LOG.error("UnknownHostException", e);
throw new NumInvalidDNSHostException("Invalid DNS host.", e);
}
LOG.info("NumAPI object created.");
}
/**
* Support multiple DNS hosts.
*
* @param dnsHosts The DNS host String arraymto override the defaults configured for the local machine.
* @throws NumInvalidDNSHostException on error
* @throws NumInvalidParameterException on error
*/
public NumAPIImpl(final String[] dnsHosts) throws NumInvalidDNSHostException, NumInvalidParameterException {
this();
LOG.info("enter - NumAPI({})", Arrays.toString(dnsHosts));
if (dnsHosts == null || dnsHosts.length == 0) {
LOG.error("No DNS hosts supplied.");
throw new NumInvalidParameterException("No DNS hosts supplied.");
}
try {
// Use a SimpleResolver if there is only one host
if (dnsHosts.length == 1) {
final String dnsHost = dnsHosts[0];
if (!StringUtils.isEmpty(dnsHost)) {
final SimpleResolver resolver = new SimpleResolver(dnsHost);
Lookup.setDefaultResolver(resolver);
} else {
LOG.error("Empty hostname in the dnsHosts parameter.");
throw new NumInvalidDNSHostException("Empty hostname in the dnsHosts parameter.");
}
} else {
final ExtendedResolver resolver = new ExtendedResolver(dnsHosts);
Lookup.setDefaultResolver(resolver);
}
} catch (UnknownHostException e) {
LOG.error("UnknownHostException", e);
throw new NumInvalidDNSHostException("Invalid DNS host.", e);
}
LOG.info("NumAPI object created.");
}
/**
* Alternative constructor used to override the default DNS host and port.
*
* @param dnsHost The DNS host to override the defaults configured for the local machine.
* @param port The port to use on the DNS host.
* @throws NumInvalidDNSHostException on error
*/
public NumAPIImpl(final String dnsHost, final int port) throws NumInvalidDNSHostException {
this();
LOG.info("NumAPI({}, {})", dnsHost, port);
try {
if (!StringUtils.isEmpty(dnsHost)) {
final SimpleResolver resolver = new SimpleResolver(dnsHost);
resolver.setPort(port);
Lookup.setDefaultResolver(resolver);
setTCPOnly(true);
}
} catch (UnknownHostException e) {
LOG.error("UnknownHostException", e);
throw new NumInvalidDNSHostException("Invalid DNS host.", e);
}
LOG.info("NumAPI object created.");
}
/**
* Tell dnsjava to use TCP and not UDP.
*
* @param flag true to use TCP only.
*/
@Override
public void setTCPOnly(final boolean flag) {
LOG.info("Use TCP only : {}", flag);
Lookup.getDefaultResolver()
.setTCP(flag);
}
/**
* Override the top-level zone from 'num.uk' to 'myzone.com' for example.
*
* @param zone The top level zone to use for DNS lookups. Replaces the default of 'num.uk'
* @throws NumInvalidParameterException if the zone is null or empty
*/
@Override
public void setTopLevelZone(final String zone) throws NumInvalidParameterException {
LOG.info("setTopLevelZone({})", zone);
if (StringUtils.isEmpty(zone)) {
throw new NumInvalidParameterException("zone cannot be null or empty");
}
appContext.stringConstants.setTopLevelZone(zone);
}
/**
* Initialise a new NumAPIContextBase object for a specific module/domain combination.
* The returned context object can be used to obtain the list of required user variables that must be set
* before moving on to retrieveNumRecord().
*
* @param moduleId E.g. "1" for the Contacts module.
* @param netString a domain name, URL, or email address that identifies the location in DNS of a NUM record.
* @param timeoutMillis the timeout in milliseconds to wait for responses from DNS.
* @return a new NumAPIContextBase object.
* @throws NumBadModuleIdException on error
* @throws NumBadModuleConfigDataException on error
* @throws NumBadURLException on error
* @throws NumInvalidParameterException on error
* @throws NumBadRecordException on error
* @throws NumDNSQueryException on error
* @throws NumInvalidDNSQueryException on error
*/
@Override
public NumAPIContext begin(final String moduleId, final String netString, final int timeoutMillis) throws
NumBadModuleIdException,
NumBadModuleConfigDataException,
NumBadURLException,
NumInvalidParameterException,
NumBadRecordException,
NumDNSQueryException,
NumInvalidDNSQueryException {
LOG.info("enter - begin({}, {}, {})", moduleId, netString, timeoutMillis);
assert moduleId != null && moduleId.trim()
.length() > 0;
assert netString != null && netString.trim()
.length() > 0;
assert timeoutMillis > 0;
// Create the context object and the validated ModuleDNSQueries object.
final NumAPIContextBase ctx = new NumAPIContextBase();
final ModuleDNSQueries moduleDNSQueries = moduleFactory.getInstance(appContext, moduleId, netString);
ctx.setModuleDNSQueries(moduleDNSQueries);
// Get the moduleDNSQueries config data
final Record[] records = dnsServices.getConfigFileTXTRecords(appContext, moduleId, timeoutMillis);
if (records == null || records.length == 0) {
LOG.error("No configuration moduleDNSQueries file available. Check that the moduleDNSQueries ID is correct.: {}", moduleId);
throw new NumBadModuleIdException("No configuration moduleDNSQueries file available. Check that the moduleDNSQueries ID is correct.: " + moduleId);
}
final String configTxt = dnsServices.rebuildTXTRecordContent(records)
.replaceAll("\\\\", "");
final ModuleConfig moduleConfig = modlServices.interpretModuleConfig(configTxt);
if (!moduleConfig.isValid()) {
LOG.error("The module config file is invalid. {}", configTxt);
throw new NumBadModuleConfigDataException("Invalid module config data: " + configTxt);
}
ctx.setModuleConfig(moduleConfig);
LOG.info("Module configuration: {}", moduleConfig);
LOG.info("exit - begin()");
return ctx;
}
/**
* This method uses the module context and the supplied Required User Variable values to obtain a fully expanded
* JSON object from DNS. The supplied handler will be notified when the results are available or an error occurs.
*
* @param ctx The context object returned by the begin() method.
* @param handler a handler object to receive the JSON results or processing errors.
* @param timeoutMillis the maximum duration of each DNS request, the total wait time could be up to 4 times this value.
* @return A Future object
*/
@Override
public Future retrieveNumRecord(final NumAPIContext ctx, final NumAPICallbacks handler, final int timeoutMillis) {
LOG.info("retrieveNumRecord()");
assert ctx != null;
assert handler != null;
// 1. Re-interpret the module config now that we have the user variables.
// 2. Get the NUM record from DNS (either independent, managed, prepopulated, or populator)
// 2. Prepend the RCF to the NUM record from DNS as a *LOAD entry (i.e. the module URL)
// 3. Run the resulting record through the Java MODL Interpreter and make the results available to the client via the handler.
// Do the rest of the operation asynchronously.
// This submits a Callable object, so exceptions should be reported to the user when they call the get() method on the Future object.
LOG.info("Submitting background query.");
final Future future = executor.submit(() -> {
final String result = numLookup(ctx, handler, timeoutMillis);
if (result == null) {
LOG.error("Unable to retrieve a NUM record.");
handler.setLocation(null);
ctx.setLocation(null);
return null;
} else {
handler.setResult(result);
return result;
}
});
LOG.info("Background query running.");
return future;
}
/**
* Main lookup method with fairly complex state behaviour to handle the various lookup locations and retry scenarios.
*
* @param ctx the NumAPIContext
* @param handler the NumAPICallbacks
* @param timeoutMillis the timeoutMillis
* @return a NUM record String
* @throws NumBadRecordException on error
* @throws NumRecordEncryptionRequiredException on error
* @throws NumNoDecryptionKeyException on error
* @throws NumUnsupportedEncryptionAlgorithmException on error
* @throws NumDecryptionException on error
* @throws NumBadURLException on error
* @throws NumInvalidRedirectException on error
* @throws NumInvalidParameterException on error
* @throws NumNotImplementedException on error
* @throws NumBadMultipartRecordException on error
* @throws NumInvalidDNSQueryException on error
* @throws NumMaximumRedirectsExceededException on error
* @throws NumNoRecordAvailableException on error
* @throws NumPopulatorErrorException on error
* @throws NumInvalidPopulatorResponseCodeException on error
*/
private String numLookup(final NumAPIContext ctx, final NumAPICallbacks handler, final int timeoutMillis) throws
NumBadRecordException,
NumRecordEncryptionRequiredException,
NumNoDecryptionKeyException,
NumUnsupportedEncryptionAlgorithmException,
NumDecryptionException,
NumBadURLException,
NumInvalidRedirectException,
NumInvalidParameterException,
NumNotImplementedException,
NumBadMultipartRecordException,
NumInvalidDNSQueryException,
NumMaximumRedirectsExceededException,
NumNoRecordAvailableException,
NumPopulatorErrorException,
NumInvalidPopulatorResponseCodeException {
final NumAPIContextBase context = (NumAPIContextBase) ctx;
context.setLocation(INDEPENDENT);
LOG.info("Trying the INDEPENDENT location.");
final ModuleConfig moduleConfig = context.getModuleConfig();
do {
try {
// Attempt to get the record from DNS
String numRecord = getNumRecord(timeoutMillis, context);
// If that failed then try the managed record.
final Module module = moduleConfig.getModule();
if (numRecord == null) {
LOG.info("Lookup returned no result.");
final boolean rootQuery = context.getModuleDNSQueries()
.isRootQuery();
switch (context.getLocation()) {
case INDEPENDENT:
LOG.info("Trying the MANAGED location.");
context.setLocation(MANAGED);
break;
case MANAGED:
if (rootQuery) {
if (module.isRprq()) {
LOG.info("Trying the POPULATED location.");
context.setLocation(POPULATED);
} else {
LOG.info("Not configured to use the POPULATED location.");
context.setLocation(STOP);
}
} else {
// Assume its a branch query
if (module.isBprq()) {
LOG.info("Trying the POPULATED location.");
context.setLocation(POPULATED);
} else {
LOG.info("Not configured to use the POPULATED location.");
context.setLocation(STOP);
}
}
break;
case POPULATED:
if (rootQuery) {
if (module.isRpsq()) {
LOG.info("Trying the POPULATOR.");
context.setLocation(POPULATOR);
} else {
LOG.info("Not configured to use the POPULATOR.");
context.setLocation(STOP);
}
} else {
// Assume its a branch query
if (module.isBpsq()) {
LOG.info("Trying the POPULATOR.");
context.setLocation(POPULATOR);
} else {
LOG.info("Not configured to use the POPULATOR.");
context.setLocation(STOP);
}
}
// fall through to the POPULATOR
case POPULATOR:
LOG.info("Trying the POPULATOR.");
final String fromPopulator = getNumRecordFromPopulator(timeoutMillis, context);
String json = interpretNumRecord(fromPopulator, context);
if (json != null) {
checkForEncryption(json, handler, context);
}
return handler.getResult();
case ENCRYPTED:
LOG.info("Processing an ENCRYPTED record.");
processDecryption(handler.getResult(), handler, context);
return handler.getResult();
case STOP:
default:
return null;
}
} else {
String json = interpretNumRecord(numRecord, context);
checkForEncryption(json, handler, context);
return handler.getResult();
}
} catch (final NumLookupRedirect numLookupRedirect) {
context.setLocation(INDEPENDENT);
context.handleQueryRedirect(appContext, numLookupRedirect.getRedirect(), context);
} catch (final NumQueryRedirect numQueryRedirect) {
context.handleQueryRedirect(appContext, numQueryRedirect.getRedirect(), context);
}
} while (true);
}
/**
* Check for and handle encrypted records. TODO: Not fully implemented in this version.
*
* @param jsonResult the interpreted and possibly encrypted JSON String
* @param handler the NumAPICallbacks
* @param context the NumAPIContextBase
* @throws NumRecordEncryptionRequiredException on error
* @throws NumUnsupportedEncryptionAlgorithmException on error
* @throws NumNoDecryptionKeyException on error
*/
private void checkForEncryption(final String jsonResult, final NumAPICallbacks handler, final NumAPIContextBase context) throws
NumRecordEncryptionRequiredException,
NumUnsupportedEncryptionAlgorithmException,
NumNoDecryptionKeyException {
if (jsonResult.contains("\"e_\"")) {
handler.setEncrypted(true);// record is currently encrypted - this will change if we can decrypt it.
handler.setWasEncrypted(true);// record was encrypted when received - this will not change.
context.setLocation(ENCRYPTED);
// Parse the JSON and extract the encrypted record.
try {
final ObjectMapper mapper = new ObjectMapper();
final JsonNode node = mapper.readTree(jsonResult);
// ---------------------------------
// Check for an encryption algorithm
// ---------------------------------
final JsonNode a_ = node.findValue("a_");
String algorithm;
if (a_ == null || a_.asText()
.trim()
.length() == 0) {
// No algorithm specified so check for a default.
algorithm = context.getModuleConfig()
.getModule()
.getDea();
if (StringUtils.isEmpty(algorithm)) {
LOG.info("No decryption algorithm specified in the NUM record and no default algorithm in the module configuration. Defaulting to AES.");
algorithm = "AES";
}
} else {
algorithm = a_.asText()
.trim();
}
// -------------------------------
// Save the full Base64 encoded MODL record.
// -------------------------------
handler.setEncryptionAlgorithm(algorithm);
handler.setResult(jsonResult);
// -------------------------------------------------
// Check whether we support the encryption algorithm
// -------------------------------------------------
if (!EncryptionUtils.isSupported(algorithm)) {
throw new NumUnsupportedEncryptionAlgorithmException(algorithm);
}
final Key key = handler.getKey();
if (key == null) {
throw new NumNoDecryptionKeyException("A decryption key is required for " + algorithm);
}
} catch (final IOException e) {
// This shouldn't happen since we generated the JSON string, so just log it rather than throw an exception
LOG.error("Error parsing JSON String.", e);
}
} else {
handler.setEncrypted(false);
handler.setWasEncrypted(false);
if (context.getModuleConfig()
.getModule()
.isRer()) {
LOG.error("Encryption is required but the record was not encrypted.");
throw new NumRecordEncryptionRequiredException("Encryption is required but the record was not encrypted.");
}
handler.setResult(jsonResult);
}
}
/**
* Decrypt an encrypted record if a key is available.
*
* @param base64value the base64 encoded and encrypted String.
* @param handler the NumAPICallbacks
* @param context the NumAPIContextBase
* @throws NumDecryptionException on error
* @throws NumNoDecryptionKeyException on error
* @throws NumBadRecordException on error
* @throws NumLookupRedirect on error
* @throws NumQueryRedirect on error
*/
@Override
public void processDecryption(final String base64value, final NumAPICallbacks handler, final NumAPIContextBase context) throws
NumDecryptionException,
NumNoDecryptionKeyException,
NumBadRecordException,
NumLookupRedirect,
NumQueryRedirect {
final String algorithm = handler.getEncryptionAlgorithm();
final byte[] raw = Base64.getDecoder()
.decode(base64value);
final Key key = handler.getKey();
if (key == null) {
throw new NumNoDecryptionKeyException("A decryption key is required for " + algorithm);
}
final String decrypted = EncryptionUtils.decrypt(raw, algorithm, key);
handler.setEncrypted(false);// We managed to decrypt the record.
interpretNumRecord(decrypted, context);
final String interpretedResult = modlServices.interpretNumRecord(decrypted);
handler.setResult(interpretedResult);
}
/**
* We had a response with the Truncated Flag set.
*
* @param timeoutMillis The number of milliseconds we're prepared to wait per DNS request.
* @param context The NumAPIContextBase
* @param recordLocation The DNS query String.
* @return An array of Record objects
* @throws NumBadMultipartRecordException on error
* @throws NumInvalidDNSQueryException on error
* @throws NumNotImplementedException on error
* @throws NumNoRecordAvailableException if a CNAME or SPF record is received instead of a TXT record
*/
private Record[] getMultiPartRecords(final int timeoutMillis, final NumAPIContextBase context, final String recordLocation) throws
NumBadMultipartRecordException,
NumInvalidDNSQueryException,
NumNotImplementedException,
NumNoRecordAvailableException {
LOG.info("getMultiPartRecords(recordLocation={})", recordLocation);
Record[] recordFromDns;// First get the number of parts and check it is valid.
final Record[] numberOfPartsRecord = dnsServices.getRecordFromDnsNoCache("0." + recordLocation, timeoutMillis, context.getModuleConfig()
.getModule()
.isDsr());
if (numberOfPartsRecord == null || numberOfPartsRecord.length == 0) {
return null;
}
final String numberOfPartsStr = dnsServices.rebuildTXTRecordContent(numberOfPartsRecord);
if (!numberOfPartsStr.startsWith("parts=")) {
throw new NumBadMultipartRecordException("Invalid record 0 for multi-part record: " + numberOfPartsStr);
}
final int numberOfParts = Integer.parseInt(numberOfPartsStr.substring(6));
if (numberOfParts > MAX_NUMBER_OF_MULTI_PARTS) {
throw new NumBadMultipartRecordException("Too many parts for a multi-part record: " + numberOfPartsStr);
}
// Now get each part and add them all to a list.
final List parts = new ArrayList<>();
for (int i = 1; i <= numberOfParts; i++) {
final Record[] partNRecords = dnsServices.getRecordFromDnsNoCache("" + i + "." + recordLocation, timeoutMillis, context.getModuleConfig()
.getModule()
.isDsr());
if (partNRecords != null && partNRecords.length > 0) {
parts.addAll(Arrays.asList(partNRecords));
}
}
recordFromDns = parts.toArray(new Record[]{});
return recordFromDns;
}
/**
* Try retrieving a record from the populator
*
* @param timeoutMillis The timeout
* @param context The context obtained from the NumAPI.begin() method
* @return The String result or null
* @throws NumPopulatorErrorException on error
* @throws NumNoRecordAvailableException on error
* @throws NumInvalidPopulatorResponseCodeException on error
* @throws NumBadMultipartRecordException on error
* @throws NumBadRecordException on error
* @throws NumNotImplementedException on error
* @throws NumInvalidDNSQueryException on error
*/
private String getNumRecordFromPopulator(int timeoutMillis, final NumAPIContextBase context) throws
NumPopulatorErrorException,
NumNoRecordAvailableException,
NumInvalidPopulatorResponseCodeException,
NumBadMultipartRecordException,
NumBadRecordException,
NumNotImplementedException,
NumInvalidDNSQueryException {
LOG.info("getNumRecordFromPopulator()");
final String recordLocation = context.getModuleDNSQueries()
.getPopulatorLocation();
LOG.info("Querying the populator service: {}", recordLocation);
String numRecord = null;
while (numRecord == null) {
numRecord = getNumRecordNoCache(timeoutMillis, context, recordLocation);
if (numRecord == null) {
// This is unrecoverable, we should get status_ or error_ object.
break;
}
LOG.info("Response from Populator: {}.", numRecord);
// Parse the MODL response
final PopulatorResponse response = modlServices.interpretPopulatorResponse(numRecord);
if (!response.isValid()) {
throw new NumInvalidPopulatorResponseCodeException("Bad response received from the populator service.");
}
// Handle the status_ response codes
if (response.getStatus_() != null) {
numRecord = handlePopulatorStatusCodes(timeoutMillis, context, response);
}
// Handle the error_ response codes
if (response.getError_() != null) {
if (response.getError_()
.getCode() == 100) {// Enter the populated zone retry loop
LOG.error("NUM Populator error: {}, {}", response.getError_()
.getCode(), response.getError_()
.getDescription());
try {
int i = 0;
while (i < PopulatorRetryConfig.ERROR_RETRIES) {
LOG.info("Sleeping for {} seconds.", PopulatorRetryConfig.ERROR_RETRY_DELAYS[i]);
TimeUnit.SECONDS.sleep(PopulatorRetryConfig.ERROR_RETRY_DELAYS[i]);
LOG.info("Retrying...");
numRecord = getNumRecord(timeoutMillis, context);
final PopulatorResponse retryResponse = modlServices.interpretPopulatorResponse(numRecord);
if (retryResponse.getStatus_() != null) {
return handlePopulatorStatusCodes(timeoutMillis, context, retryResponse);
}
i++;
}
} catch (final InterruptedException e) {
LOG.error("Interrupted", e);
}
LOG.error("Cannot retrieve NUM record from any location.");
throw new NumNoRecordAvailableException("Cannot retrieve NUM record from any location.");
} else {
LOG.error("NUM Populator error: {}, {}", response.getError_()
.getCode(), response.getError_()
.getDescription());
throw new NumPopulatorErrorException(response.getError_()
.getDescription());
}
}
}
return numRecord;
}
/**
* Populator status codes tell us how to retry the queries while the populator works in the background to get the necessary data
*
* @param timeoutMillis the timeout
* @param context the NumAPIContextBase object.
* @param response the response from the populator
* @return null or a valid NUM record
* @throws NumNoRecordAvailableException on error
* @throws NumInvalidPopulatorResponseCodeException on error
* @throws NumBadMultipartRecordException on error
* @throws NumInvalidDNSQueryException on error
* @throws NumNotImplementedException on error
*/
private String handlePopulatorStatusCodes(int timeoutMillis, NumAPIContextBase context, PopulatorResponse response) throws
NumNoRecordAvailableException,
NumInvalidPopulatorResponseCodeException,
NumBadMultipartRecordException,
NumInvalidDNSQueryException,
NumNotImplementedException {
LOG.info("handlePopulatorStatusCodes()");
String numRecord = null;
switch (response.getStatus_()
.getCode()) {
case 1:
LOG.info("Populator Status code: 1");
// Enter the populated zone retry loop
try {
//
// In a change to the specification, we're going to retry the POPULATOR rather than the POPULATED
// zone because that will be the first to respond when a scraper completes.
//
context.setLocation(POPULATOR);
int i = 0;
while (i < PopulatorRetryConfig.RETRY_DELAYS.length) {
LOG.info("Sleeping for {} seconds.", PopulatorRetryConfig.RETRY_DELAYS[i]);
TimeUnit.SECONDS.sleep(PopulatorRetryConfig.RETRY_DELAYS[i]);
LOG.info("Retrying...");
numRecord = getNumRecord(timeoutMillis, context);
if (numRecord != null && !numRecord.contains("status_") && !numRecord.contains("error_")) {
return numRecord;
}
i++;
}
LOG.error("Cannot retrieve NUM record from any location.");
throw new NumNoRecordAvailableException("Cannot retrieve NUM record from any location.");
} catch (InterruptedException e) {
break;
}
case 2:
LOG.info("Populator Status code: 2");
// The record is available at the authoritative server
context.setLocation(INDEPENDENT);
numRecord = getNumRecord(timeoutMillis, context);
if (numRecord == null) {
LOG.error("Cannot retrieve NUM record from any location.");
throw new NumNoRecordAvailableException("Cannot retrieve NUM record from any location.");
}
break;
case 3:
LOG.info("Populator Status code: 3");
// The record exists in the managed zone.
context.setLocation(MANAGED);
numRecord = getNumRecord(timeoutMillis, context);
if (numRecord == null) {
LOG.error("Cannot retrieve NUM record from any location.");
throw new NumNoRecordAvailableException("Cannot retrieve NUM record from any location.");
}
break;
case PopulatorResponse.VALID_TXT_RECORD_CODE:
numRecord = response.getNumRecord();
break;
default:
context.setLocation(null);
LOG.error("Invalid response code from DNS populator service: {}", response.getStatus_()
.getCode());
throw new NumInvalidPopulatorResponseCodeException("Invalid response code from DNS populator service: " + response.getStatus_()
.getCode());
}
return numRecord;
}
/**
* Interpret the supplied NUM record from DNS using the RCF value from the ModuleConfig object.
*
* @param moduleNumber The module number
* @param moduleConfig The ModuleConfig
* @param numRecord The NUM record from DNS
* @return The JSON String result of the fully expanded NUM record.
* @throws NumBadRecordException on error
* @throws NumQueryRedirect on error
* @throws NumLookupRedirect on error
*/
private String getInterpretedNumRecordAsJson(final String moduleNumber, final ModuleConfig moduleConfig, final String numRecord) throws
NumBadRecordException,
NumQueryRedirect,
NumLookupRedirect {
LOG.info("getInterpretedNumRecordAsJson({}, moduleConfig, {})", moduleNumber, numRecord);
final StringBuilder numRecordBuffer = new StringBuilder();
final RequiredUserVariable[] ruv = moduleConfig.getModule()
.getRuv();
if (ruv != null) {
for (RequiredUserVariable v : ruv) {
numRecordBuffer.append(v.getKey());
numRecordBuffer.append("=");
numRecordBuffer.append(v.getValue());
numRecordBuffer.append(";");
}
}
numRecordBuffer.append("*load=\"http://modules.num.uk/");
numRecordBuffer.append(moduleNumber);
numRecordBuffer.append("/rcf.txt!\";");
numRecordBuffer.append(numRecord);
LOG.info("Interpret NUM record: {}", numRecordBuffer.toString());
return modlServices.interpretNumRecord(numRecordBuffer.toString());
}
/**
* Convert a NUM record String to an interpreted JSON String. Handle any redirect instructions in the interpreted MODL record
*
* @param numRecord the uninterpreted NUM record.
* @param context the NumAPIContext
* @return the interpreted NUM record as a JSON string.
* @throws NumLookupRedirect on error
* @throws NumBadRecordException on error
* @throws NumQueryRedirect on error
*/
private String interpretNumRecord(final String numRecord, final NumAPIContextBase context) throws NumLookupRedirect,
NumBadRecordException,
NumQueryRedirect {
LOG.info("interpretNumRecord({}, context)", numRecord);
String json = null;
if (numRecord != null && numRecord.trim()
.length() > 0) {
// Build a MODL object using the required user variables, the RCF, and the NUM record from DNS.
json = getInterpretedNumRecordAsJson(context.getModuleDNSQueries()
.getModuleId(), context.getModuleConfig(), numRecord);
}
return json;
}
/**
* Get a NUM record for the given query string. Try multi-part queries if necessary.
*
* @param timeoutMillis The timeout
* @param context The context obtained from the NumAPI.begin() method
* @return The raw NUM record from DNS.
* @throws NumBadMultipartRecordException on error
* @throws NumInvalidDNSQueryException on error
* @throws NumNotImplementedException on error
* @throws NumNoRecordAvailableException if a CNAME or SPF record is received instead of a TXT record
*/
private String getNumRecord(int timeoutMillis, NumAPIContextBase context) throws
NumBadMultipartRecordException,
NumInvalidDNSQueryException,
NumNotImplementedException,
NumNoRecordAvailableException {
final String recordLocation = context.getRecordLocation();
LOG.info("getNumRecord({}, context, {})", timeoutMillis, recordLocation);
Record[] recordFromDns;
try {
recordFromDns = dnsServices.getRecordFromDnsNoCache(recordLocation, timeoutMillis, context.getModuleConfig()
.getModule()
.isDsr());
} catch (final PossibleMultiPartRecordException e) {
LOG.info("Possible multi-part record - checking.");
recordFromDns = getMultiPartRecords(timeoutMillis, context, recordLocation);
}
if (recordFromDns == null || recordFromDns.length == 0) {
return null;
}
return dnsServices.rebuildTXTRecordContent(recordFromDns);
}
/**
* Get a NUM record for the given query string. Try multi-part queries if necessary. Don't cache the responses
*
* @param timeoutMillis The timeout
* @param context The context obtained from the NumAPI.begin() method
* @param recordLocation The DNS query String.
* @return The raw NUM record from DNS.
* @throws NumBadMultipartRecordException on error
* @throws NumInvalidDNSQueryException on error
* @throws NumNotImplementedException on error
* @throws NumNoRecordAvailableException if a CNAME or SPF record is received instead of a TXT record
*/
private String getNumRecordNoCache(int timeoutMillis, NumAPIContextBase context, String recordLocation) throws
NumBadMultipartRecordException,
NumInvalidDNSQueryException,
NumNotImplementedException,
NumNoRecordAvailableException {
LOG.info("getNumRecordNoCache({}, context, {})", timeoutMillis, recordLocation);
Record[] recordFromDns;
try {
recordFromDns = dnsServices.getRecordFromDnsNoCache(recordLocation, timeoutMillis, context.getModuleConfig()
.getModule()
.isDsr());
} catch (final PossibleMultiPartRecordException e) {
LOG.info("Possible multi-part record - checking.");
recordFromDns = getMultiPartRecords(timeoutMillis, context, recordLocation);
}
if (recordFromDns == null || recordFromDns.length == 0) {
return null;
}
return dnsServices.rebuildTXTRecordContent(recordFromDns);
}
/**
* Stop any outstanding DNS queries still in the Executor.
*/
@Override
public void shutdown() {
LOG.info("shutdown()");
try {
executor.shutdown();
executor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOG.error("Shutdown interrupted: ", e);
} finally {
if (!executor.isTerminated()) {
LOG.info("Failed to shutdown after 1 second, so forcing shutdown.");
executor.shutdownNow();
}
}
LOG.info("Shutdown complete.");
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy