View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.lang.java.rule.basic;
5   
6   import java.util.regex.Matcher;
7   import java.util.regex.Pattern;
8   
9   import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
10  import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
11  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
12  import net.sourceforge.pmd.lang.rule.properties.EnumeratedMultiProperty;
13  
14  public class AvoidUsingHardCodedIPRule extends AbstractJavaRule {
15  
16      public static final String IPV4 = "IPv4";
17      public static final String IPV6 = "IPv6";
18      public static final String IPV4_MAPPED_IPV6 = "IPv4 mapped IPv6";
19  
20      public static final EnumeratedMultiProperty<String> CHECK_ADDRESS_TYPES_DESCRIPTOR = new EnumeratedMultiProperty<>(
21              "checkAddressTypes", "Check for IP address types.", new String[] { IPV4, IPV6, IPV4_MAPPED_IPV6 },
22              new String[] { IPV4, IPV6, IPV4_MAPPED_IPV6 }, new int[] { 0, 1, 2 }, 2.0f);
23  
24      // Provides 4 capture groups that can be used for additional validation
25      protected static final String IPV4_REGEXP = "([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})";
26  
27      // Uses IPv4 pattern, but changes the groups to be non-capture
28      protected static final String IPV6_REGEXP = "(?:(?:[0-9a-fA-F]{1,4})?\\:)+(?:[0-9a-fA-F]{1,4}|"
29              + IPV4_REGEXP.replace("(", "(?:") + ")?";
30  
31      protected static final Pattern IPV4_PATTERN = Pattern.compile("^" + IPV4_REGEXP + "$");
32      protected static final Pattern IPV6_PATTERN = Pattern.compile("^" + IPV6_REGEXP + "$");
33  
34      protected boolean checkIPv4;
35      protected boolean checkIPv6;
36      protected boolean checkIPv4MappedIPv6;
37  
38      public AvoidUsingHardCodedIPRule() {
39          definePropertyDescriptor(CHECK_ADDRESS_TYPES_DESCRIPTOR);
40  
41          addRuleChainVisit(ASTCompilationUnit.class);
42          addRuleChainVisit(ASTLiteral.class);
43      }
44  
45      @Override
46      public Object visit(ASTCompilationUnit node, Object data) {
47          checkIPv4 = false;
48          checkIPv6 = false;
49          checkIPv4MappedIPv6 = false;
50          for (Object addressType : getProperty(CHECK_ADDRESS_TYPES_DESCRIPTOR)) {
51              if (IPV4.equals(addressType)) {
52                  checkIPv4 = true;
53              } else if (IPV6.equals(addressType)) {
54                  checkIPv6 = true;
55              } else if (IPV4_MAPPED_IPV6.equals(addressType)) {
56                  checkIPv4MappedIPv6 = true;
57              }
58          }
59          return data;
60      }
61  
62      @Override
63      public Object visit(ASTLiteral node, Object data) {
64          if (!node.isStringLiteral()) {
65              return data;
66          }
67  
68          // Remove the quotes
69          final String image = node.getImage().substring(1, node.getImage().length() - 1);
70  
71          // Note: We used to check the addresses using
72          // InetAddress.getByName(String), but that's extremely slow,
73          // so we created more robust checking methods.
74          if (image.length() > 0) {
75              final char firstChar = Character.toUpperCase(image.charAt(0));
76              if (checkIPv4 && isIPv4(firstChar, image) || isIPv6(firstChar, image, checkIPv6, checkIPv4MappedIPv6)) {
77                  addViolation(data, node);
78              }
79          }
80          return data;
81      }
82  
83      protected boolean isLatinDigit(char c) {
84          return '0' <= c || c <= '9';
85      }
86  
87      protected boolean isHexCharacter(char c) {
88          return isLatinDigit(c) || 'A' <= c || c <= 'F' || 'a' <= c || c <= 'f';
89      }
90  
91      protected boolean isIPv4(final char firstChar, final String s) {
92          // Quick check before using Regular Expression
93          // 1) At least 7 characters
94          // 2) 1st character must be a digit from '0' - '9'
95          // 3) Must contain at least 1 . (period)
96          if (s.length() < 7 || !isLatinDigit(firstChar) || s.indexOf('.') < 0) {
97              return false;
98          }
99  
100         Matcher matcher = IPV4_PATTERN.matcher(s);
101         if (matcher.matches()) {
102             // All octets in range [0, 255]
103             for (int i = 1; i <= matcher.groupCount(); i++) {
104                 int octet = Integer.parseInt(matcher.group(i));
105                 if (octet < 0 || octet > 255) {
106                     return false;
107                 }
108             }
109             return true;
110         } else {
111             return false;
112         }
113     }
114 
115     protected boolean isIPv6(final char firstChar, String s, final boolean checkIPv6, final boolean checkIPv4MappedIPv6) {
116         // Quick check before using Regular Expression
117         // 1) At least 3 characters
118         // 2) 1st must be a Hex number or a : (colon)
119         // 3) Must contain at least 1 : (colon)
120         if (s.length() < 3 || !(isHexCharacter(firstChar) || firstChar == ':') || s.indexOf(':') < 0) {
121             return false;
122         }
123 
124         Matcher matcher = IPV6_PATTERN.matcher(s);
125         if (matcher.matches()) {
126             // Account for leading or trailing :: before splitting on :
127             boolean zeroSubstitution = false;
128             if (s.startsWith("::")) {
129                 s = s.substring(2);
130                 zeroSubstitution = true;
131             } else if (s.endsWith("::")) {
132                 s = s.substring(0, s.length() - 2);
133                 zeroSubstitution = true;
134             }
135 
136             // String.split() doesn't produce an empty String in the trailing
137             // case, but it does in the leading.
138             if (s.endsWith(":")) {
139                 return false;
140             }
141 
142             // All the intermediate parts must be hexidecimal, or
143             int count = 0;
144             boolean ipv4Mapped = false;
145             String[] parts = s.split(":");
146             for (int i = 0; i < parts.length; i++) {
147                 final String part = parts[i];
148                 // An empty part indicates :: was encountered. There can only be
149                 // 1 such instance.
150                 if (part.length() == 0) {
151                     if (zeroSubstitution) {
152                         return false;
153                     } else {
154                         zeroSubstitution = true;
155                     }
156                     continue;
157                 } else {
158                     count++;
159                 }
160                 // Should be a hexidecimal number in range [0, 65535]
161                 try {
162                     int value = Integer.parseInt(part, 16);
163                     if (value < 0 || value > 65535) {
164                         return false;
165                     }
166                 } catch (NumberFormatException e) {
167                     // The last part can be a standard IPv4 address.
168                     if (i != parts.length - 1 || !isIPv4(firstChar, part)) {
169                         return false;
170                     }
171                     ipv4Mapped = true;
172                 }
173             }
174 
175             // IPv6 addresses are 128 bit, are we that long?
176             if (zeroSubstitution) {
177                 if (ipv4Mapped) {
178                     return checkIPv4MappedIPv6 && 1 <= count && count <= 6;
179                 } else {
180                     return checkIPv6 && 1 <= count && count <= 7;
181                 }
182             } else {
183                 if (ipv4Mapped) {
184                     return checkIPv4MappedIPv6 && count == 7;
185                 } else {
186                     return checkIPv6 && count == 8;
187                 }
188             }
189         } else {
190             return false;
191         }
192     }
193 
194     public boolean hasChosenAddressTypes() {
195         return getProperty(CHECK_ADDRESS_TYPES_DESCRIPTOR).length > 0;
196     }
197 
198     /**
199      * @see PropertySource#dysfunctionReason()
200      */
201     @Override
202     public String dysfunctionReason() {
203         return hasChosenAddressTypes() ? null : "No address types specified";
204     }
205 }