/* * The MIT License * * Copyright 2013 Tim Boudreau. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.mastfrog.url; import org.netbeans.validation.api.Validating; import com.mastfrog.util.AbstractBuilder; import com.mastfrog.util.Checks; import com.mastfrog.util.collections.CollectionUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; import org.netbeans.validation.api.Problems; import org.netbeans.validation.api.Validator; import org.netbeans.validation.api.builtin.stringvalidation.StringValidators; import org.openide.util.NbBundle; /** * An internet host such as a host name or IP address. Validation is more * extensive than that done by java.net.URL, including checking name and label * lengths for spec compatibility (max 63 chars per label, 255 chars per host * name). * * @author Tim Boudreau */ public final class Host implements URLComponent, Validating, Iterable<Label> { static final Pattern IPV6_REGEX = Pattern.compile("(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); private static final long serialVersionUID = 1L; final boolean ipv6; private final Label[] labels; public Host(boolean ipv6, Label... labels) { Checks.notNull("domains", labels); Checks.noNullElements("domains", labels); this.ipv6 = ipv6; this.labels = new Label[labels.length]; System.arraycopy(labels, 0, this.labels, 0, labels.length); } public Host(Label... labels) { this(false, labels); } public int size() { return labels.length; } public Label getElement(int ix) { return labels[ix]; } public boolean isDomain(String domain) { Checks.notNull("domain", domain); Host host = Host.parse(domain); boolean result = true; int labelCount = host.size() - 1; int mySize = size() - 1; if (labelCount < 0 || mySize < 0) { return false; } do { result &= host.getElement(labelCount).equals(getElement(mySize)); if (!result) { break; } labelCount--; mySize--; } while (labelCount >= 0 && mySize >= 0); return result; } public static Host parse(String path) { if (IPV6_REGEX.matcher(path).matches() || path.startsWith("::")) { String[] parts = path.split(":"); Label[] labels = new Label[parts.length]; for (int i = 0; i < parts.length; i++) { labels[i] = new Label(parts[i]); } return new Host(true, labels); } String[] parts = path.split("\\" + URLBuilder.LABEL_DELIMITER); Label[] els = new Label[parts.length]; for (int i = 0; i < parts.length; i++) { // PENDING: Percent encode characters as UTF8, per // http://tools.ietf.org/html/rfc3986 and // http://tools.ietf.org/html/rfc3490 els[i] = new Label(parts[i]); } return new Host(els); } public Label getTopLevelDomain() { if (isIpAddress()) { return null; } return labels.length > 1 ? labels[labels.length - 1] : null; } public Label getDomain() { if (isIpAddress()) { return null; } return labels.length > 1 ? labels[labels.length - 2] : null; } public Label[] getLabels() { Label[] result = new Label[labels.length]; for (int i = 0; i < result.length; i++) { result[i] = labels[labels.length - (1 + i)]; } return result; } public Host getParentDomain() { if (labels.length > 1) { Label[] l = new Label[labels.length - 1]; System.arraycopy(labels, 1, l, 0, l.length); return new Host(l); } else { return null; } } public boolean isIpAddress() { boolean result = labels.length > 0; if (result) { boolean ipv6Found = false; for (Label label : labels) { boolean isIpv6 = label.isValidIpV6Component(); boolean isIpv4 = label.isValidIpV4Component(); ipv6Found |= isIpv6; result = isIpv4 || isIpv6; if (!result) { break; } } if (result && ipv6Found && labels.length > 8) { return false; } else if (!ipv6 && (result && labels.length != 4)) { return false; } } return result; } public boolean isIpV4Address() { boolean result = labels.length == 4; if (result) { for (Label label : labels) { result = label.isValidIpV4Component(); if (!result) { break; } } } return result; } public boolean isIpV6Address() { boolean result = ipv6 && labels.length > 0 && labels.length <= 8; if (result) { for (Label label : labels) { result &= label.isValidIpV6Component(); if (!result) { break; } } } return result; } public int length() { int len = 0; for (Label dom : labels) { boolean hadContents = len > 0; if (hadContents) { len++; } len += dom.length(); } return len; } @Override public boolean isValid() { if (isLocalhost() && !"".equals(toString())) { return true; } if (isIpV6Address()) { return true; } boolean ip = isIpAddress(); boolean result = ip || (getTopLevelDomain() != null && getDomain() != null); if (result) { boolean someNumeric = false; boolean allNumeric = true; for (Label d : labels) { result &= d.isValid(); boolean num = d.isNumeric(); allNumeric &= num; someNumeric |= num; } if (someNumeric && !allNumeric) { return false; } } if (result) { int length = length(); result = length <= 255 && length > 0; } if (result && ip) { int sz = size(); result = sz == 4 || sz == 6; } if (result) { Problems p = getProblems(); result = p == null ? result : !p.hasFatal(); } return result; } @Override public Problems getProblems() { if (isLocalhost() && !"".equals(toString())) { return null; } String s = toString(); Problems problems = new Problems(); Validator<String> validator = isIpV4Address() ? StringValidators.IP_ADDRESS : StringValidators.HOST_NAME; // Host name handles ipv6 validator.validate(problems, getComponentName(), s); if (isIpV6Address()) { return problems; } boolean isIp = isIpAddress(); if (problems.allProblems().isEmpty()) { if (!isIp && getTopLevelDomain() == null) { problems.append(NbBundle.getMessage(Host.class, "TopLevelDomainMissing")); } if (!isIp && getDomain() == null) { problems.append(NbBundle.getMessage(Host.class, "DomainMissing")); } if (length() > 255) { problems.append(NbBundle.getMessage(Host.class, "HostTooLong")); } if (isIpV4Address() && size() != 4) { problems.append(NbBundle.getMessage(Host.class, "WrongNumberOfElementsForIpAddress")); } boolean someNumeric = false; boolean allNumeric = true; for (Label d : labels) { boolean num = d.isNumeric(); allNumeric &= num; someNumeric |= num; } if (someNumeric && !allNumeric) { problems.append(NbBundle.getMessage(Host.class, "HostMixesNumericAndNonNumeric")); } } return problems.hasFatal() ? problems : null; } public static AbstractBuilder<Label, Host> builder() { return new HostBuilder(); } @Override public String getComponentName() { return NbBundle.getMessage(Host.class, "host"); } @Override public String toString() { StringBuilder sb = new StringBuilder(); boolean leadingEmpty = false; for (int i = 0; i < labels.length; i++) { Label label = labels[i]; if (ipv6) { if (i == 0 && label.isEmpty()) { leadingEmpty = true; } } if (sb.length() > 0) { sb.append(ipv6 ? ':' : URLBuilder.LABEL_DELIMITER); } label.appendTo(sb); } if (ipv6 && leadingEmpty) { sb.insert(0, "::"); } return sb.toString(); } @Override public void appendTo(StringBuilder sb) { sb.append(toString()); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Host other = (Host) obj; if (isLocalhost() && other.isLocalhost()) { return true; } if (ipv6 || (isIpV4Address() && other.isIpV4Address()) || (isIpV6Address() && other.isIpV6Address())) { return Arrays.equals(toIntArray(), other.toIntArray()); } return Arrays.equals(this.labels, other.labels); } private String arr2s() { StringBuilder sb = new StringBuilder(); for (int i : toIntArray()) { if (sb.length() > 0) { sb.append(":"); } sb.append(Integer.toHexString(i)); } return sb.toString(); } private boolean isLocalhostIpV6() { if (!isIpV6Address()) { return false; } boolean result = labels.length <= 8; if (result) { for (int i = 0; i < labels.length; i++) { int intValue = labels[i].asInt(true); if (i == labels.length - 1) { result &= intValue == 1; } else { result &= intValue == 0; } if (!result) { break; } } } return result; } boolean isIpv6() { return ipv6; } public boolean isLocalhost() { if (labels.length == 1 && labels[0].toString().isEmpty()) { return true; } if (labels.length == 1 && "localhost".equals(labels[0].toString())) { return true; } String stringValue = toString(); return "127.0.0.1".equals(stringValue) || "::1".equals(stringValue) || isLocalhostIpV6(); } @Override public int hashCode() { if (isLocalhost()) { return 1; } if (isIpAddress()) { return Arrays.hashCode(toIntArray()); } int hash = 5; hash = 73 * hash + Arrays.deepHashCode(this.labels); return hash; } private int[] findLongestZeroRunStart(int[] ints) { int length = 0; int start = -1; int currStart = -1; boolean inRun = false; for (int i = 0; i < ints.length; i++) { boolean isZero = ints[i] == 0; if (!inRun && isZero) { currStart = i; inRun = true; continue; } boolean endOfRun = inRun && !isZero; if (endOfRun) { inRun = false; if (i - currStart > length) { length = i - currStart; start = currStart; } } } if (length == 1 || length == 0) { return new int[]{-1, 0}; } return new int[]{start, start + length}; } public Host canonicalize() { if (isLocalhost()) { if (labels.length == 0 || (labels.length == 1 && labels[0].getLabel().equals(""))) { return this; } return ipv6 ? new Host(true, new Label(""), new Label(""), new Label("1")) : new Host(ipv6, new Label("127"), new Label("0"), new Label("0"), new Label("1")); } // This follows the recommendations in // https://en.wikipedia.org/wiki/IPv6_address - i.e. // a single 0 is not compressed; the longest run of zeros is // compressed, and if there are two runs of equal length, the // leftmost run of zeros is compressed if (isIpV6Address()) { List<Label> result = new ArrayList<>(8); int[] ints = toIntArray(); int[] skip = findLongestZeroRunStart(ints); boolean allZeros = true; for (int i = 0; i < ints.length; i++) { allZeros &= ints[i] == 0; if (i == skip[0]) { result.add(new Label("")); } if (skip[0] != -1 && i >= skip[0] && i < skip[1]) { continue; } result.add(new Label(Integer.toHexString(ints[i]))); } if (allZeros) { return new Host(true, new Label(""), new Label("")); } return new Host(true, result.toArray(new Label[result.size()])); } return this; } @Override public Iterator<Label> iterator() { return CollectionUtils.toIterator(labels); } public int[] toIntArray() { int[] result = new int[ipv6 ? 8 : 4]; Iterator<Label> forward = CollectionUtils.toIterator(labels); int remaining = labels.length; int ix = 0; while (forward.hasNext()) { Label label = forward.next(); if (label.isEmpty()) { break; } result[ix++] = label.asInt(ipv6); remaining--; } ix = result.length - 1; if (remaining > 0) { Iterator<Label> backward = CollectionUtils.toReverseIterator(labels); while (remaining > 0 && ix > 0) { result[ix--] = backward.next().asInt(ipv6); remaining--; } } return result; } private static final class HostBuilder extends AbstractBuilder<Label, Host> { @Override public Host create() { return create(false); } public Host create(boolean ipv6) { Label[] domains = elements().toArray(new Label[size()]); Label[] reversed = new Label[domains.length]; for (int i = 0; i < domains.length; i++) { reversed[i] = domains[domains.length - (i + 1)]; } return new Host(reversed); } @Override protected Label createElement(String string) { return new Label(string); } } }