/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.rule.basic; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.pmd.PropertySource; import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit; import net.sourceforge.pmd.lang.java.ast.ASTLiteral; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; import net.sourceforge.pmd.lang.rule.properties.EnumeratedMultiProperty; public class AvoidUsingHardCodedIPRule extends AbstractJavaRule { public static final String IPV4 = "IPv4"; public static final String IPV6 = "IPv6"; public static final String IPV4_MAPPED_IPV6 = "IPv4 mapped IPv6"; public static final EnumeratedMultiProperty<String> CHECK_ADDRESS_TYPES_DESCRIPTOR = new EnumeratedMultiProperty<>( "checkAddressTypes", "Check for IP address types.", new String[] { IPV4, IPV6, IPV4_MAPPED_IPV6 }, new String[] { IPV4, IPV6, IPV4_MAPPED_IPV6 }, new int[] { 0, 1, 2 }, 2.0f); // Provides 4 capture groups that can be used for additional validation protected static final String IPV4_REGEXP = "([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})"; // Uses IPv4 pattern, but changes the groups to be non-capture protected static final String IPV6_REGEXP = "(?:(?:[0-9a-fA-F]{1,4})?\\:)+(?:[0-9a-fA-F]{1,4}|" + IPV4_REGEXP.replace("(", "(?:") + ")?"; protected static final Pattern IPV4_PATTERN = Pattern.compile("^" + IPV4_REGEXP + "$"); protected static final Pattern IPV6_PATTERN = Pattern.compile("^" + IPV6_REGEXP + "$"); protected boolean checkIPv4; protected boolean checkIPv6; protected boolean checkIPv4MappedIPv6; public AvoidUsingHardCodedIPRule() { definePropertyDescriptor(CHECK_ADDRESS_TYPES_DESCRIPTOR); addRuleChainVisit(ASTCompilationUnit.class); addRuleChainVisit(ASTLiteral.class); } @Override public Object visit(ASTCompilationUnit node, Object data) { checkIPv4 = false; checkIPv6 = false; checkIPv4MappedIPv6 = false; for (Object addressType : getProperty(CHECK_ADDRESS_TYPES_DESCRIPTOR)) { if (IPV4.equals(addressType)) { checkIPv4 = true; } else if (IPV6.equals(addressType)) { checkIPv6 = true; } else if (IPV4_MAPPED_IPV6.equals(addressType)) { checkIPv4MappedIPv6 = true; } } return data; } @Override public Object visit(ASTLiteral node, Object data) { if (!node.isStringLiteral()) { return data; } // Remove the quotes final String image = node.getImage().substring(1, node.getImage().length() - 1); // Note: We used to check the addresses using // InetAddress.getByName(String), but that's extremely slow, // so we created more robust checking methods. if (image.length() > 0) { final char firstChar = Character.toUpperCase(image.charAt(0)); if (checkIPv4 && isIPv4(firstChar, image) || isIPv6(firstChar, image, checkIPv6, checkIPv4MappedIPv6)) { addViolation(data, node); } } return data; } protected boolean isLatinDigit(char c) { return '0' <= c || c <= '9'; } protected boolean isHexCharacter(char c) { return isLatinDigit(c) || 'A' <= c || c <= 'F' || 'a' <= c || c <= 'f'; } protected boolean isIPv4(final char firstChar, final String s) { // Quick check before using Regular Expression // 1) At least 7 characters // 2) 1st character must be a digit from '0' - '9' // 3) Must contain at least 1 . (period) if (s.length() < 7 || !isLatinDigit(firstChar) || s.indexOf('.') < 0) { return false; } Matcher matcher = IPV4_PATTERN.matcher(s); if (matcher.matches()) { // All octets in range [0, 255] for (int i = 1; i <= matcher.groupCount(); i++) { int octet = Integer.parseInt(matcher.group(i)); if (octet < 0 || octet > 255) { return false; } } return true; } else { return false; } } protected boolean isIPv6(final char firstChar, String s, final boolean checkIPv6, final boolean checkIPv4MappedIPv6) { // Quick check before using Regular Expression // 1) At least 3 characters // 2) 1st must be a Hex number or a : (colon) // 3) Must contain at least 1 : (colon) if (s.length() < 3 || !(isHexCharacter(firstChar) || firstChar == ':') || s.indexOf(':') < 0) { return false; } Matcher matcher = IPV6_PATTERN.matcher(s); if (matcher.matches()) { // Account for leading or trailing :: before splitting on : boolean zeroSubstitution = false; if (s.startsWith("::")) { s = s.substring(2); zeroSubstitution = true; } else if (s.endsWith("::")) { s = s.substring(0, s.length() - 2); zeroSubstitution = true; } // String.split() doesn't produce an empty String in the trailing // case, but it does in the leading. if (s.endsWith(":")) { return false; } // All the intermediate parts must be hexidecimal, or int count = 0; boolean ipv4Mapped = false; String[] parts = s.split(":"); for (int i = 0; i < parts.length; i++) { final String part = parts[i]; // An empty part indicates :: was encountered. There can only be // 1 such instance. if (part.length() == 0) { if (zeroSubstitution) { return false; } else { zeroSubstitution = true; } continue; } else { count++; } // Should be a hexidecimal number in range [0, 65535] try { int value = Integer.parseInt(part, 16); if (value < 0 || value > 65535) { return false; } } catch (NumberFormatException e) { // The last part can be a standard IPv4 address. if (i != parts.length - 1 || !isIPv4(firstChar, part)) { return false; } ipv4Mapped = true; } } // IPv6 addresses are 128 bit, are we that long? if (zeroSubstitution) { if (ipv4Mapped) { return checkIPv4MappedIPv6 && 1 <= count && count <= 6; } else { return checkIPv6 && 1 <= count && count <= 7; } } else { if (ipv4Mapped) { return checkIPv4MappedIPv6 && count == 7; } else { return checkIPv6 && count == 8; } } } else { return false; } } public boolean hasChosenAddressTypes() { return getProperty(CHECK_ADDRESS_TYPES_DESCRIPTOR).length > 0; } /** * @see PropertySource#dysfunctionReason() */ @Override public String dysfunctionReason() { return hasChosenAddressTypes() ? null : "No address types specified"; } }