001/*
002 * PermissionsEx
003 * Copyright (C) zml and PermissionsEx contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ca.stellardrift.permissionsex.impl.util;
018
019import org.checkerframework.checker.nullness.qual.Nullable;
020import org.spongepowered.configurate.ConfigurationNode;
021import org.spongepowered.configurate.serialize.SerializationException;
022import org.spongepowered.configurate.serialize.TypeSerializer;
023
024import java.lang.reflect.Type;
025import java.net.Inet4Address;
026import java.net.Inet6Address;
027import java.net.InetAddress;
028import java.net.UnknownHostException;
029import java.util.function.Predicate;
030
031import static java.util.Objects.requireNonNull;
032
033public final class IpSet implements Predicate<InetAddress> {
034    public static final TypeSerializer<IpSet> SERIALIZER = new IpSetSerializer();
035    private final InetAddress address;
036    private final int prefixLength;
037
038    /**
039     * Create an IP set matching only one single address.
040     *
041     * @param address the address to match
042     * @return a new ip set
043     */
044    public static IpSet only(final InetAddress address) {
045        requireNonNull(address, "address");
046        return new IpSet(address, maxPrefixLength(address));
047    }
048
049    /**
050     * Resolve an ip set from address and prefix length
051     * @param address the IP address
052     * @param prefixLen the prefix length
053     * @return a new ip set
054     * @throws IllegalArgumentException if the prefix length is not valid for the provided address
055     */
056    public static IpSet fromAddrPrefix(final InetAddress address, final int prefixLen) {
057        requireNonNull(address, "address");
058        validatePrefixLength(address, prefixLen);
059        return new IpSet(address, prefixLen);
060    }
061
062    /**
063     * Given a CIDR range, construct an IP set.
064     * @param spec the specification
065     * @return a parsed ip set matching the range
066     * @throws IllegalArgumentException if the syntax is not valid for CIDR, or the prefix length is invalid.
067     */
068    public static IpSet fromCidr(final String spec) {
069        requireNonNull(spec, "spec");
070
071        final @Nullable String addrString;
072        final int prefixLen;
073        final int slashIndex = spec.lastIndexOf("/");
074        if (slashIndex == -1) {
075            addrString = spec;
076            prefixLen = addrString.startsWith("[") ? 128 : 32; // are we IPv6?
077        } else {
078            addrString = spec.substring(0, slashIndex);
079            prefixLen = Integer.parseInt(spec.substring(slashIndex + 1));
080        }
081        final InetAddress addr;
082        try {
083            addr = InetAddress.getByName(addrString);
084        } catch (final UnknownHostException ex) {
085            throw new IllegalArgumentException(addrString + "does not contain a valid IP address");
086        }
087        return fromAddrPrefix(addr, prefixLen);
088    }
089
090    IpSet(final InetAddress address, final int prefixLength) {
091        this.address = address;
092        this.prefixLength = prefixLength;
093    }
094
095    // State validation
096
097    static int maxPrefixLength(final InetAddress address) {
098        if (address instanceof Inet4Address) {
099            return 32;
100        } else if (address instanceof Inet6Address) {
101            return 128;
102        } else {
103            throw new IllegalArgumentException("Unknown IP address type " + address.getClass());
104        }
105    }
106
107    static void validatePrefixLength(final InetAddress address, final int prefixLength) {
108        if (prefixLength < 0) {
109            throw new IllegalArgumentException("Minimum prefix length for an IP address is 0, but "
110                    + prefixLength + " was provided.");
111        }
112        final int maxLength = maxPrefixLength(address);
113        if (prefixLength > maxLength) {
114            throw new IllegalArgumentException("Maximum prefix length for a "
115                    + address.getClass().getSimpleName() + " is " + maxLength + ", but "
116                    + prefixLength + " was provided");
117        }
118    }
119
120    @Override
121    public boolean test(final InetAddress address) {
122        return this.contains(address);
123    }
124
125    public boolean contains(final InetAddress input) {
126        final byte[] address = input.getAddress();
127        final byte[] checkAddr = this.address.getAddress();
128        if (address.length != checkAddr.length) {
129            return false;
130        }
131        final byte completeSegments = (byte) (this.prefixLength >> 3);
132        final byte overlap = (byte) (this.prefixLength & 7);
133        for (int i = 0; i < completeSegments; ++i) {
134            if (address[i] != checkAddr[i]) {
135                return false;
136            }
137        }
138
139        for (int i = 0; i < overlap; ++i) {
140            if (((checkAddr[completeSegments + 1] >> (7 - i)) & 0x1)
141                    != ((address[completeSegments + 1] >> (7 - i)) & 0x1)) {
142                return false;
143            }
144        }
145        return true;
146    }
147
148    public boolean contains(final IpSet other) {
149        if (other.prefixLength < this.prefixLength) {
150            return false;
151        }
152
153        return other.address.equals(this.address)
154                || this.contains(other.address);
155    }
156
157    /**
158     * Get the CIDR string representation of this IP set.
159     *
160     * @return the representation
161     */
162    @Override
163    public String toString() {
164        return this.address.getHostAddress() + "/" + this.prefixLength;
165    }
166
167    static class IpSetSerializer implements TypeSerializer<IpSet> {
168
169        IpSetSerializer() {
170        }
171
172        @Override
173        public IpSet deserialize(final Type type, final ConfigurationNode node) throws SerializationException {
174            try {
175                return IpSet.fromCidr(node.getString());
176            } catch (final IllegalArgumentException ex) {
177                throw new SerializationException(ex);
178            }
179        }
180
181        @Override
182        public void serialize(final Type type, final @Nullable IpSet obj, final ConfigurationNode node) throws SerializationException {
183            if (obj == null) {
184                node.raw(null);
185                return;
186            }
187
188            node.set(obj.toString()); // to cidr
189        }
190    }
191
192}