All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.maxmind.db.Reader Maven / Gradle / Ivy

package com.maxmind.db;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Instances of this class provide a reader for the MaxMind DB format. IP
 * addresses can be looked up using the get method.
 */
public final class Reader implements Closeable {
    private static final int IPV4_LEN = 4;
    private static final int DATA_SECTION_SEPARATOR_SIZE = 16;
    private static final byte[] METADATA_START_MARKER = {(byte) 0xAB,
        (byte) 0xCD, (byte) 0xEF, 'M', 'a', 'x', 'M', 'i', 'n', 'd', '.',
        'c', 'o', 'm'};

    private final int ipV4Start;
    private final Metadata metadata;
    private final AtomicReference bufferHolderReference;
    private final NodeCache cache;
    private final ConcurrentHashMap constructors;

    /**
     * The file mode to use when opening a MaxMind DB.
     */
    public enum FileMode {
        /**
         * The default file mode. This maps the database to virtual memory. This
         * often provides similar performance to loading the database into real
         * memory without the overhead.
         */
        MEMORY_MAPPED,
        /**
         * Loads the database into memory when the reader is constructed.
         */
        MEMORY
    }

    /**
     * Constructs a Reader for the MaxMind DB format, with no caching. The file
     * passed to it must be a valid MaxMind DB file such as a GeoIP2 database
     * file.
     *
     * @param database the MaxMind DB file to use.
     * @throws IOException if there is an error opening or reading from the file.
     */
    public Reader(File database) throws IOException {
        this(database, NoCache.getInstance());
    }

    /**
     * Constructs a Reader for the MaxMind DB format, with the specified backing
     * cache. The file passed to it must be a valid MaxMind DB file such as a
     * GeoIP2 database file.
     *
     * @param database the MaxMind DB file to use.
     * @param cache    backing cache instance
     * @throws IOException if there is an error opening or reading from the file.
     */
    public Reader(File database, NodeCache cache) throws IOException {
        this(database, FileMode.MEMORY_MAPPED, cache);
    }

    /**
     * Constructs a Reader with no caching, as if in mode
     * {@link FileMode#MEMORY}, without using a File instance.
     *
     * @param source the InputStream that contains the MaxMind DB file.
     * @throws IOException if there is an error reading from the Stream.
     */
    public Reader(InputStream source) throws IOException {
        this(source, NoCache.getInstance());
    }

    /**
     * Constructs a Reader with the specified backing cache, as if in mode
     * {@link FileMode#MEMORY}, without using a File instance.
     *
     * @param source the InputStream that contains the MaxMind DB file.
     * @param cache  backing cache instance
     * @throws IOException if there is an error reading from the Stream.
     */
    public Reader(InputStream source, NodeCache cache) throws IOException {
        this(new BufferHolder(source), "", cache);
    }

    /**
     * Constructs a Reader for the MaxMind DB format, with no caching. The file
     * passed to it must be a valid MaxMind DB file such as a GeoIP2 database
     * file.
     *
     * @param database the MaxMind DB file to use.
     * @param fileMode the mode to open the file with.
     * @throws IOException if there is an error opening or reading from the file.
     */
    public Reader(File database, FileMode fileMode) throws IOException {
        this(database, fileMode, NoCache.getInstance());
    }

    /**
     * Constructs a Reader for the MaxMind DB format, with the specified backing
     * cache. The file passed to it must be a valid MaxMind DB file such as a
     * GeoIP2 database file.
     *
     * @param database the MaxMind DB file to use.
     * @param fileMode the mode to open the file with.
     * @param cache    backing cache instance
     * @throws IOException if there is an error opening or reading from the file.
     */
    public Reader(File database, FileMode fileMode, NodeCache cache) throws IOException {
        this(new BufferHolder(database, fileMode), database.getName(), cache);
    }

    private Reader(BufferHolder bufferHolder, String name, NodeCache cache) throws IOException {
        this.bufferHolderReference = new AtomicReference<>(
            bufferHolder);

        if (cache == null) {
            throw new NullPointerException("Cache cannot be null");
        }
        this.cache = cache;

        ByteBuffer buffer = bufferHolder.get();
        int start = this.findMetadataStart(buffer, name);

        Decoder metadataDecoder = new Decoder(this.cache, buffer, start);
        this.metadata = metadataDecoder.decode(start, Metadata.class);

        this.ipV4Start = this.findIpV4StartNode(buffer);

        this.constructors = new ConcurrentHashMap<>();
    }

    /**
     * Looks up ipAddress in the MaxMind DB.
     *
     * @param        the type to populate.
     * @param ipAddress the IP address to look up.
     * @param cls       the class of object to populate.
     * @return the object.
     * @throws IOException if a file I/O error occurs.
     */
    public  T get(InetAddress ipAddress, Class cls) throws IOException {
        return getRecord(ipAddress, cls).getData();
    }

    int getIpv4Start() {
        return this.ipV4Start;
    }

    /**
     * Looks up ipAddress in the MaxMind DB.
     *
     * @param        the type to populate.
     * @param ipAddress the IP address to look up.
     * @param cls       the class of object to populate.
     * @return the record for the IP address. If there is no data for the
     * address, the non-null {@link DatabaseRecord} will still be returned.
     * @throws IOException if a file I/O error occurs.
     */
    public  DatabaseRecord getRecord(InetAddress ipAddress, Class cls)
        throws IOException {

        byte[] rawAddress = ipAddress.getAddress();

        int[] traverseResult = traverseTree(rawAddress, rawAddress.length * 8);

        int pl = traverseResult[1];
        int record = traverseResult[0];

        int nodeCount = this.metadata.getNodeCount();
        ByteBuffer buffer = this.getBufferHolder().get();
        T dataRecord = null;
        if (record > nodeCount) {
            // record is a data pointer
            try {
                dataRecord = this.resolveDataPointer(buffer, record, cls);
            } catch (DeserializationException exception) {
                throw new DeserializationException(
                    "Error getting record for IP " + ipAddress + " -  " + exception.getMessage(),
                    exception);
            }
        }
        return new DatabaseRecord<>(dataRecord, ipAddress, pl);
    }

    /**
     * Creates a Networks iterator and skips aliased networks.
     * Please note that a MaxMind DB may map IPv4 networks into several locations
     * in an IPv6 database. networks() iterates over the canonical locations and
     * not the aliases. To include the aliases, you can set includeAliasedNetworks to true.
     * 
     * @param  Represents the data type(e.g., Map, HastMap, etc.).
     * @param typeParameterClass The type of data returned by the iterator.
     * @return Networks The Networks iterator.
     * @throws InvalidNetworkException Exception for using an IPv6 network in ipv4-only database.
     * @throws ClosedDatabaseException Exception for a closed databased.
     * @throws InvalidDatabaseException Exception for an invalid database.
     */
    public  Networks networks(Class typeParameterClass) throws 
        InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException {
        return this.networks(false, typeParameterClass);
    }

    /**
     * Creates a Networks iterator.
     * Please note that a MaxMind DB may map IPv4 networks into several locations
     * in an IPv6 database. This iterator will iterate over all of these locations
     * separately. To set the iteration over the IPv4 networks once, use the
     * includeAliasedNetworks option.
     * 
     * @param  Represents the data type(e.g., Map, HastMap, etc.).
     * @param includeAliasedNetworks Enable including aliased networks.
     * @return Networks The Networks iterator.
     * @throws InvalidNetworkException Exception for using an IPv6 network in ipv4-only database.
     * @throws ClosedDatabaseException Exception for a closed databased.
     * @throws InvalidDatabaseException Exception for an invalid database.
     */
    public  Networks networks(
            boolean includeAliasedNetworks,
            Class typeParameterClass) throws
        InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException {
        try {
            if (this.getMetadata().getIpVersion() == 6) {
                InetAddress ipv6 = InetAddress.getByAddress(new byte[16]);
                Network ipAllV6 = new Network(ipv6, 0); // Mask 128.
                return this.networksWithin(ipAllV6, includeAliasedNetworks, typeParameterClass);
            }

            InetAddress ipv4 = InetAddress.getByAddress(new byte[4]);
            Network ipAllV4 = new Network(ipv4, 0); // Mask 32.
            return this.networksWithin(ipAllV4, includeAliasedNetworks, typeParameterClass);
        } catch (UnknownHostException e) {
            /* This is returned by getByAddress. This should never happen
            as the ipv4 and ipv6 are constants set by us. */
            return null;
        }
    }

    BufferHolder getBufferHolder() throws ClosedDatabaseException {
        BufferHolder bufferHolder = this.bufferHolderReference.get();
        if (bufferHolder == null) {
            throw new ClosedDatabaseException();
        }
        return bufferHolder;
    }

    private int startNode(int bitLength) {
        // Check if we are looking up an IPv4 address in an IPv6 tree. If this
        // is the case, we can skip over the first 96 nodes.
        if (this.metadata.getIpVersion() == 6 && bitLength == 32) {
            return this.ipV4Start;
        }
        // The first node of the tree is always node 0, at the beginning of the
        // value
        return 0;
    }

    private int findIpV4StartNode(ByteBuffer buffer)
        throws InvalidDatabaseException {
        if (this.metadata.getIpVersion() == 4) {
            return 0;
        }

        int node = 0;
        for (int i = 0; i < 96 && node < this.metadata.getNodeCount(); i++) {
            node = this.readNode(buffer, node, 0);
        }
        return node;
    }

    /**
     * Returns an iterator within the specified network.
     * Please note that a MaxMind DB may map IPv4 networks into several locations
     * in an IPv6 database. This iterator will iterate over all of these locations
     * separately. To only iterate over the IPv4 networks once, use the
     * includeAliasedNetworks option.
     * @param  Represents the data type(e.g., Map, HastMap, etc.).
     * @param network Specifies the network to be iterated.
     * @param includeAliasedNetworks Boolean for including aliased networks.
     * @param typeParameterClass The type of data returned by the iterator.
     * @return Networks
     * @throws InvalidNetworkException Exception for using an IPv6 network in ipv4-only database.
     * @throws ClosedDatabaseException Exception for a closed databased.
     * @throws InvalidDatabaseException Exception for an invalid database.
     */
    public  Networks networksWithin(
            Network network,
            boolean includeAliasedNetworks,
            Class typeParameterClass)
        throws InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException {
        InetAddress networkAddress = network.getNetworkAddress();
        if (this.metadata.getIpVersion() == 4 && networkAddress instanceof Inet6Address) {
            throw new InvalidNetworkException(networkAddress);
        }

        byte[] ipBytes = networkAddress.getAddress();
        int prefixLength = network.getPrefixLength();

        if (this.metadata.getIpVersion() == 6 && ipBytes.length == IPV4_LEN) {
            if (includeAliasedNetworks) {
                // Convert it to the IP address (in 16-byte from) of the IPv4 address.
                ipBytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                    -1, -1, // -1 is for 0xff.
                    ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3]};
            } else {
                ipBytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                    ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3] };
            }
            prefixLength += 96;
        }

        int[] traverseResult = this.traverseTree(ipBytes, prefixLength);
        int node = traverseResult[0];
        int prefix = traverseResult[1];

        return new Networks<>(this, includeAliasedNetworks,
            new Networks.NetworkNode[] {new Networks.NetworkNode(ipBytes, prefix, node)},
            typeParameterClass);
    }

    /**
     * Returns the node number and the prefix for the network.
     * @param ip The ip address to traverse.
     * @param bitCount The prefix.
     * @return int[]
     */
    private int[] traverseTree(byte[] ip, int bitCount)
        throws ClosedDatabaseException, InvalidDatabaseException {
        ByteBuffer buffer = this.getBufferHolder().get();
        int bitLength = ip.length * 8;
        int record = this.startNode(bitLength);
        int nodeCount = this.metadata.getNodeCount();

        int i = 0;
        for (; i < bitCount && record < nodeCount; i++) {
            int b = 0xFF & ip[i / 8];
            int bit = 1 & (b >> 7 - (i % 8));

            // bit:0 -> left record.
            // bit:1 -> right record.
            record = this.readNode(buffer, record, bit);
        }

        return new int[]{record, i};
    }

    int readNode(ByteBuffer buffer, int nodeNumber, int index)
            throws InvalidDatabaseException {
        // index is the index of the record within the node, which
        // can either be 0 or 1.
        int baseOffset = nodeNumber * this.metadata.getNodeByteSize();

        switch (this.metadata.getRecordSize()) {
            case 24:
                // For a 24 bit record, each record is 3 bytes.
                buffer.position(baseOffset + index * 3);
                return Decoder.decodeInteger(buffer, 0, 3);
            case 28:
                int middle = buffer.get(baseOffset + 3);

                if (index == 0) {
                    // We get the most significant from the first half
                    // of the byte. It belongs to the first record.
                    middle = (0xF0 & middle) >>> 4;
                } else {
                    // We get the most significant byte of the second record.
                    middle = 0x0F & middle;
                }
                buffer.position(baseOffset + index * 4);
                return Decoder.decodeInteger(buffer, middle, 3);
            case 32:
                buffer.position(baseOffset + index * 4);
                return Decoder.decodeInteger(buffer, 0, 4);
            default:
                throw new InvalidDatabaseException("Unknown record size: "
                    + this.metadata.getRecordSize());
        }
    }

     T resolveDataPointer(
        ByteBuffer buffer,
        int pointer,
        Class cls
    ) throws IOException {
        int resolved = (pointer - this.metadata.getNodeCount())
            + this.metadata.getSearchTreeSize();

        if (resolved >= buffer.capacity()) {
            throw new InvalidDatabaseException(
                "The MaxMind DB file's search tree is corrupt: "
                    + "contains pointer larger than the database.");
        }

        // We only want the data from the decoder, not the offset where it was
        // found.
        Decoder decoder = new Decoder(
            this.cache,
            buffer,
            this.metadata.getSearchTreeSize() + DATA_SECTION_SEPARATOR_SIZE,
            this.constructors
        );
        return decoder.decode(resolved, cls);
    }

    /*
     * Apparently searching a file for a sequence is not a solved problem in
     * Java. This searches from the end of the file for metadata start.
     *
     * This is an extremely naive but reasonably readable implementation. There
     * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
     * an issue, but I suspect it won't be.
     */
    private int findMetadataStart(ByteBuffer buffer, String databaseName)
        throws InvalidDatabaseException {
        int fileSize = buffer.capacity();

        FILE:
        for (int i = 0; i < fileSize - METADATA_START_MARKER.length + 1; i++) {
            for (int j = 0; j < METADATA_START_MARKER.length; j++) {
                byte b = buffer.get(fileSize - i - j - 1);
                if (b != METADATA_START_MARKER[METADATA_START_MARKER.length - j
                    - 1]) {
                    continue FILE;
                }
            }
            return fileSize - i;
        }
        throw new InvalidDatabaseException(
            "Could not find a MaxMind DB metadata marker in this file ("
                + databaseName + "). Is this a valid MaxMind DB file?");
    }

    /**
     * @return the metadata for the MaxMind DB file.
     */
    public Metadata getMetadata() {
        return this.metadata;
    }

    /**
     * 

* Closes the database. *

*

* If you are using FileMode.MEMORY_MAPPED, this will * not unmap the underlying file due to a limitation in Java's * MappedByteBuffer. It will however set the reference to * the buffer to null, allowing the garbage collector to * collect it. *

* * @throws IOException if an I/O error occurs. */ @Override public void close() throws IOException { this.bufferHolderReference.set(null); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy