/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.nifi.lookup.maxmind;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.annotation.lifecycle.OnEnabled;
import org.apache.nifi.annotation.lifecycle.OnStopped;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.controller.AbstractControllerService;
import org.apache.nifi.controller.ConfigurationContext;
import org.apache.nifi.lookup.LookupFailureException;
import org.apache.nifi.lookup.RecordLookupService;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.serialization.record.MapRecord;
import org.apache.nifi.serialization.record.Record;
import org.apache.nifi.util.StopWatch;
import com.maxmind.geoip2.model.AnonymousIpResponse;
import com.maxmind.geoip2.model.CityResponse;
import com.maxmind.geoip2.model.ConnectionTypeResponse;
import com.maxmind.geoip2.model.ConnectionTypeResponse.ConnectionType;
import com.maxmind.geoip2.model.DomainResponse;
import com.maxmind.geoip2.model.IspResponse;
import com.maxmind.geoip2.record.Country;
import com.maxmind.geoip2.record.Location;
import com.maxmind.geoip2.record.Subdivision;
@Tags({"lookup", "enrich", "ip", "geo", "ipgeo", "maxmind", "isp", "domain", "cellular", "anonymous", "tor"})
@CapabilityDescription("A lookup service that provides several types of enrichment information for IP addresses. The service is configured by providing a MaxMind "
+ "Database file and specifying which types of enrichment should be provided for an IP Address. Each type of enrichment is a separate lookup, so configuring the "
+ "service to provide all of the available enrichment data may be slower than returning only a portion of the available enrichments. View the Usage of this component "
+ "and choose to view Additional Details for more information, such as the Schema that pertains to the information that is returned.")
public class IPLookupService extends AbstractControllerService implements RecordLookupService {
private volatile DatabaseReader databaseReader = null;
static final PropertyDescriptor GEO_DATABASE_FILE = new PropertyDescriptor.Builder()
.name("database-file")
.displayName("MaxMind Database File")
.description("Path to Maxmind IP Enrichment Database File")
.required(true)
.addValidator(StandardValidators.FILE_EXISTS_VALIDATOR)
.build();
static final PropertyDescriptor LOOKUP_CITY = new PropertyDescriptor.Builder()
.name("lookup-city")
.displayName("Lookup Geo Enrichment")
.description("Specifies whether or not information about the geographic information, such as cities, corresponding to the IP address should be returned")
.allowableValues("true", "false")
.defaultValue("true")
.expressionLanguageSupported(false)
.required(true)
.build();
static final PropertyDescriptor LOOKUP_ISP = new PropertyDescriptor.Builder()
.name("lookup-isp")
.displayName("Lookup ISP")
.description("Specifies whether or not information about the Information Service Provider corresponding to the IP address should be returned")
.expressionLanguageSupported(false)
.allowableValues("true", "false")
.defaultValue("false")
.required(true)
.build();
static final PropertyDescriptor LOOKUP_DOMAIN = new PropertyDescriptor.Builder()
.name("lookup-domain")
.displayName("Lookup Domain Name")
.description("Specifies whether or not information about the Domain Name corresponding to the IP address should be returned. "
+ "If true, the lookup will contain second-level domain information, such as foo.com but will not contain bar.foo.com")
.expressionLanguageSupported(false)
.allowableValues("true", "false")
.defaultValue("false")
.required(true)
.build();
static final PropertyDescriptor LOOKUP_CONNECTION_TYPE = new PropertyDescriptor.Builder()
.name("lookup-connection-type")
.displayName("Lookup Connection Type")
.description("Specifies whether or not information about the Connection Type corresponding to the IP address should be returned. "
+ "If true, the lookup will contain a 'connectionType' field that (if populated) will contain a value of 'Dialup', 'Cable/DSL', 'Corporate', or 'Cellular'")
.expressionLanguageSupported(false)
.allowableValues("true", "false")
.defaultValue("false")
.required(true)
.build();
static final PropertyDescriptor LOOKUP_ANONYMOUS_IP_INFO = new PropertyDescriptor.Builder()
.name("lookup-anonymous-ip")
.displayName("Lookup Anonymous IP Information")
.description("Specifies whether or not information about whether or not the IP address belongs to an anonymous network should be returned.")
.expressionLanguageSupported(false)
.allowableValues("true", "false")
.defaultValue("false")
.required(true)
.build();
@Override
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
final List<PropertyDescriptor> properties = new ArrayList<>();
properties.add(GEO_DATABASE_FILE);
properties.add(LOOKUP_CITY);
properties.add(LOOKUP_ISP);
properties.add(LOOKUP_DOMAIN);
properties.add(LOOKUP_CONNECTION_TYPE);
properties.add(LOOKUP_ANONYMOUS_IP_INFO);
return properties;
}
@OnEnabled
public void onEnabled(final ConfigurationContext context) throws IOException {
final String dbFileString = context.getProperty(GEO_DATABASE_FILE).getValue();
final File dbFile = new File(dbFileString);
final StopWatch stopWatch = new StopWatch(true);
final DatabaseReader reader = new DatabaseReader.Builder(dbFile).build();
stopWatch.stop();
getLogger().info("Completed loading of Maxmind Database. Elapsed time was {} milliseconds.", new Object[] {stopWatch.getDuration(TimeUnit.MILLISECONDS)});
databaseReader = reader;
}
@OnStopped
public void closeReader() throws IOException {
final DatabaseReader reader = databaseReader;
if (reader != null) {
reader.close();
}
}
@Override
public Optional<Record> lookup(final String key) throws LookupFailureException {
if (key == null) {
return Optional.empty();
}
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(key);
} catch (final IOException ioe) {
getLogger().warn("Could not resolve the IP for value '{}'. This is usually caused by issue resolving the appropriate DNS record or " +
"providing the service with an invalid IP address", new Object[] {key}, ioe);
return Optional.empty();
}
final Record geoRecord;
if (getProperty(LOOKUP_CITY).asBoolean()) {
final CityResponse cityResponse;
try {
cityResponse = databaseReader.city(inetAddress);
} catch (final Exception e) {
throw new LookupFailureException("Failed to lookup City information for IP Address " + inetAddress, e);
}
geoRecord = createRecord(cityResponse);
} else {
geoRecord = null;
}
final Record ispRecord;
if (getProperty(LOOKUP_ISP).asBoolean()) {
final IspResponse ispResponse;
try {
ispResponse = databaseReader.isp(inetAddress);
} catch (final Exception e) {
throw new LookupFailureException("Failed to lookup ISP information for IP Address " + inetAddress, e);
}
ispRecord = createRecord(ispResponse);
} else {
ispRecord = null;
}
final String domainName;
if (getProperty(LOOKUP_DOMAIN).asBoolean()) {
final DomainResponse domainResponse;
try {
domainResponse = databaseReader.domain(inetAddress);
} catch (final Exception e) {
throw new LookupFailureException("Failed to lookup Domain information for IP Address " + inetAddress, e);
}
domainName = domainResponse == null ? null : domainResponse.getDomain();
} else {
domainName = null;
}
final String connectionType;
if (getProperty(LOOKUP_CONNECTION_TYPE).asBoolean()) {
final ConnectionTypeResponse connectionTypeResponse;
try {
connectionTypeResponse = databaseReader.connectionType(inetAddress);
} catch (final Exception e) {
throw new LookupFailureException("Failed to lookup Domain information for IP Address " + inetAddress, e);
}
if (connectionTypeResponse == null) {
connectionType = null;
} else {
final ConnectionType type = connectionTypeResponse.getConnectionType();
connectionType = type == null ? null : type.name();
}
} else {
connectionType = null;
}
final Record anonymousIpRecord;
if (getProperty(LOOKUP_ANONYMOUS_IP_INFO).asBoolean()) {
final AnonymousIpResponse anonymousIpResponse;
try {
anonymousIpResponse = databaseReader.anonymousIp(inetAddress);
} catch (final Exception e) {
throw new LookupFailureException("Failed to lookup Anonymous IP Information for IP Address " + inetAddress, e);
}
anonymousIpRecord = createRecord(anonymousIpResponse);
} else {
anonymousIpRecord = null;
}
return Optional.ofNullable(createContainerRecord(geoRecord, ispRecord, domainName, connectionType, anonymousIpRecord));
}
private Record createRecord(final CityResponse city) {
if (city == null) {
return null;
}
final Map<String, Object> values = new HashMap<>();
values.put(CitySchema.CITY.getFieldName(), city.getCity().getName());
final Location location = city.getLocation();
values.put(CitySchema.ACCURACY.getFieldName(), location.getAccuracyRadius());
values.put(CitySchema.METRO_CODE.getFieldName(), location.getMetroCode());
values.put(CitySchema.TIMEZONE.getFieldName(), location.getTimeZone());
values.put(CitySchema.LATITUDE.getFieldName(), location.getLatitude());
values.put(CitySchema.LONGITUDE.getFieldName(), location.getLongitude());
values.put(CitySchema.CONTINENT.getFieldName(), city.getContinent().getName());
values.put(CitySchema.POSTALCODE.getFieldName(), city.getPostal().getCode());
values.put(CitySchema.COUNTRY.getFieldName(), createRecord(city.getCountry()));
final Object[] subdivisions = new Object[city.getSubdivisions().size()];
int i = 0;
for (final Subdivision subdivision : city.getSubdivisions()) {
subdivisions[i++] = createRecord(subdivision);
}
values.put(CitySchema.SUBDIVISIONS.getFieldName(), subdivisions);
return new MapRecord(CitySchema.GEO_SCHEMA, values);
}
private Record createRecord(final Subdivision subdivision) {
if (subdivision == null) {
return null;
}
final Map<String, Object> values = new HashMap<>(2);
values.put(CitySchema.SUBDIVISION_NAME.getFieldName(), subdivision.getName());
values.put(CitySchema.SUBDIVISION_ISO.getFieldName(), subdivision.getIsoCode());
return new MapRecord(CitySchema.SUBDIVISION_SCHEMA, values);
}
private Record createRecord(final Country country) {
if (country == null) {
return null;
}
final Map<String, Object> values = new HashMap<>(2);
values.put(CitySchema.COUNTRY_NAME.getFieldName(), country.getName());
values.put(CitySchema.COUNTRY_ISO.getFieldName(), country.getIsoCode());
return new MapRecord(CitySchema.COUNTRY_SCHEMA, values);
}
private Record createRecord(final IspResponse isp) {
if (isp == null) {
return null;
}
final Map<String, Object> values = new HashMap<>(4);
values.put(IspSchema.ASN.getFieldName(), isp.getAutonomousSystemNumber());
values.put(IspSchema.ASN_ORG.getFieldName(), isp.getAutonomousSystemOrganization());
values.put(IspSchema.NAME.getFieldName(), isp.getIsp());
values.put(IspSchema.ORG.getFieldName(), isp.getOrganization());
return new MapRecord(IspSchema.ISP_SCHEMA, values);
}
private Record createRecord(final AnonymousIpResponse anonymousIp) {
if (anonymousIp == null) {
return null;
}
final Map<String, Object> values = new HashMap<>(5);
values.put(AnonymousIpSchema.ANONYMOUS.getFieldName(), anonymousIp.isAnonymous());
values.put(AnonymousIpSchema.ANONYMOUS_VPN.getFieldName(), anonymousIp.isAnonymousVpn());
values.put(AnonymousIpSchema.HOSTING_PROVIDER.getFieldName(), anonymousIp.isHostingProvider());
values.put(AnonymousIpSchema.PUBLIC_PROXY.getFieldName(), anonymousIp.isPublicProxy());
values.put(AnonymousIpSchema.TOR_EXIT_NODE.getFieldName(), anonymousIp.isTorExitNode());
return new MapRecord(AnonymousIpSchema.ANONYMOUS_IP_SCHEMA, values);
}
private Record createContainerRecord(final Record geoRecord, final Record ispRecord, final String domainName, final String connectionType, final Record anonymousIpRecord) {
final Map<String, Object> values = new HashMap<>(4);
values.put("geo", geoRecord);
values.put("isp", ispRecord);
values.put("domainName", domainName);
values.put("connectionType", connectionType);
values.put("anonymousIp", anonymousIpRecord);
final Record containerRecord = new MapRecord(ContainerSchema.CONTAINER_SCHEMA, values);
return containerRecord;
}
}