001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v2.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.core.util;
015
016import java.net.InetAddress;
017import java.net.UnknownHostException;
018
019/**
020 * Matches an {@link InetAddress} against a single IP address or a CIDR network
021 * range.
022 * <p>
023 * Accepted forms:
024 * <ul>
025 * <li>Single address: {@code 192.168.1.10}, {@code 2001:db8::1}</li>
026 * <li>CIDR range: {@code 192.168.1.0/24}, {@code 2001:db8::/32}</li>
027 * </ul>
028 *
029 * @author Ceki G&uuml;lc&uuml;
030 * @since 1.6.2
031 */
032public class IpAddressMatcher {
033
034    private final String original;
035    private final byte[] networkAddress;
036    private final int prefixLength;
037
038    /**
039     * Creates a matcher for the given address or CIDR specification.
040     *
041     * @param addressOrCidr a single IP or {@code address/prefixLength}
042     * @throws IllegalArgumentException if the specification is invalid
043     */
044    public IpAddressMatcher(String addressOrCidr) {
045        if (addressOrCidr == null) {
046            throw new IllegalArgumentException("addressOrCidr must not be null");
047        }
048        this.original = addressOrCidr.trim();
049        if (this.original.isEmpty()) {
050            throw new IllegalArgumentException("addressOrCidr must not be empty");
051        }
052
053        String addressPart;
054        Integer explicitPrefix = null;
055        int slashIndex = this.original.indexOf('/');
056        if (slashIndex >= 0) {
057            addressPart = this.original.substring(0, slashIndex).trim();
058            String prefixPart = this.original.substring(slashIndex + 1).trim();
059            if (addressPart.isEmpty() || prefixPart.isEmpty()) {
060                throw new IllegalArgumentException("Invalid CIDR specification [" + addressOrCidr + "]");
061            }
062            try {
063                explicitPrefix = Integer.parseInt(prefixPart);
064            } catch (NumberFormatException e) {
065                throw new IllegalArgumentException(
066                        "Invalid prefix length in [" + addressOrCidr + "]", e);
067            }
068        } else {
069            addressPart = this.original;
070        }
071
072        InetAddress parsed;
073        try {
074            parsed = InetAddress.getByName(addressPart);
075        } catch (UnknownHostException e) {
076            throw new IllegalArgumentException("Unknown host or invalid IP address [" + addressPart + "]", e);
077        }
078
079        this.networkAddress = parsed.getAddress();
080        int maxPrefix = this.networkAddress.length * 8;
081        if (explicitPrefix != null) {
082            if (explicitPrefix < 0 || explicitPrefix > maxPrefix) {
083                throw new IllegalArgumentException("Prefix length " + explicitPrefix
084                        + " is out of range for address [" + addressPart + "] (0-" + maxPrefix + ")");
085            }
086            this.prefixLength = explicitPrefix;
087        } else {
088            this.prefixLength = maxPrefix;
089        }
090    }
091
092    /**
093     * Returns {@code true} if the given address matches this matcher.
094     *
095     * @param address the address to test; may be {@code null}
096     * @return {@code true} if {@code address} matches
097     */
098    public boolean matches(InetAddress address) {
099        if (address == null) {
100            return false;
101        }
102        return matchesBytes(address.getAddress());
103    }
104
105    /**
106     * Returns {@code true} if the given address string matches this matcher.
107     *
108     * @param address an IP address string
109     * @return {@code true} if the address matches
110     * @throws IllegalArgumentException if {@code address} cannot be parsed
111     */
112    public boolean matches(String address) {
113        if (address == null) {
114            throw new IllegalArgumentException("address must not be null");
115        }
116        try {
117            return matches(InetAddress.getByName(address.trim()));
118        } catch (UnknownHostException e) {
119            throw new IllegalArgumentException("Unknown host or invalid IP address [" + address + "]", e);
120        }
121    }
122
123    private boolean matchesBytes(byte[] candidate) {
124        if (candidate == null || candidate.length != networkAddress.length) {
125            return false;
126        }
127
128        int fullBytes = prefixLength / 8;
129        int remainingBits = prefixLength % 8;
130
131        for (int i = 0; i < fullBytes; i++) {
132            if (candidate[i] != networkAddress[i]) {
133                return false;
134            }
135        }
136
137        if (remainingBits > 0) {
138            int mask = 0xFF << (8 - remainingBits);
139            if ((candidate[fullBytes] & mask) != (networkAddress[fullBytes] & mask)) {
140                return false;
141            }
142        }
143
144        return true;
145    }
146
147    /**
148     * The original specification used to construct this matcher.
149     */
150    public String getSpecification() {
151        return original;
152    }
153
154    @Override
155    public String toString() {
156        return "IpAddressMatcher[" + original + "]";
157    }
158}