blob: 0aab54c927c6c3ac1351053f1ff20f25800bfcf7 [file] [log] [blame]
// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.enterprise.secmgr.common;
import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import org.springframework.mock.web.MockHttpServletRequest;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Static methods for fetching pages via HTTP. See RFCs 2616 and 2617 for
* details.
*/
public final class HttpUtil {
public static final String HTTP_METHOD_GET = "GET";
public static final String HTTP_METHOD_POST = "POST";
public static final String HTTP_METHOD_HEAD = "HEAD";
// HTTP header names.
public static final String HTTP_HEADER_ACCEPT = "Accept";
public static final String HTTP_HEADER_ACCEPT_CHARSET = "Accept-Charset";
public static final String HTTP_HEADER_ACCEPT_ENCODING = "Accept-Encoding";
public static final String HTTP_HEADER_ACCEPT_LANGUAGE = "Accept-Language";
public static final String HTTP_HEADER_AUTHORIZATION = "Authorization";
public static final String HTTP_HEADER_CONNECTION = "Connection";
public static final String HTTP_HEADER_CONTENT_LENGTH = "Content-Length";
public static final String HTTP_HEADER_COOKIE = "Cookie";
public static final String HTTP_HEADER_DATE = "Date";
public static final String HTTP_HEADER_LOCATION = "Location";
public static final String HTTP_HEADER_PROXY_AUTHENTICATE = "Proxy-Authenticate";
public static final String HTTP_HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization";
public static final String HTTP_HEADER_RANGE = "Range";
public static final String HTTP_HEADER_SET_COOKIE = "Set-Cookie";
public static final String HTTP_HEADER_SET_COOKIE2 = "Set-Cookie2";
public static final String HTTP_HEADER_USER_AGENT = "User-Agent";
public static final String HTTP_HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
// TODO(kstillson): The rest of this file is general purpose http, but
// this cookie-cracking code is quite security manager specific. At some
// point, it would be good to refactor (perhaps providing a caller
// specified callback for custom header processing?)
//
// TODO(kstillson): make this extensible/configurable (at least from spring).
// These are Google-specific headers set in a response, which can set the
// username and groups list for a credentials group.
public static final String COOKIE_CRACK_USERNAME_HEADER = "X-Username";
public static final String COOKIE_CRACK_GROUPS_HEADER = "X-Groups";
// Boilerplate HTTP header values.
public static final String KEEP_ALIVE = "keep-alive";
// TODO(michellez): make this configurable through spring.
public static final String USER_AGENT = "SecMgr";
public static final String ACCEPT =
"text/html, text/xhtml;q=0.9, text/plain;q=0.5, text/*;q=0.1";
public static final String ACCEPT_FOR_HEAD = "*/*";
public static final String ACCEPT_CHARSET = "us-ascii, iso-8859-1, utf-8";
public static final String ACCEPT_ENCODING = "identity";
public static final String ACCEPT_LANGUAGE = "en-us, en;q=0.9";
private static final String RANGE_FORMAT = "bytes=0-%d";
public static final char PARAM_VALUE_SEPARATOR = '=';
public static final char STRING_DELIMITER = '"';
public static final char STRING_QUOTE = '\\';
public static final char PARAM_SEPARATOR_CHAR = ';';
public static final String PARAM_SEPARATOR = "; ";
// Don't instantiate.
private HttpUtil() {
throw new UnsupportedOperationException();
}
public static boolean isHttpGetMethod(String method) {
return HTTP_METHOD_GET.equalsIgnoreCase(method);
}
public static boolean isHttpPostMethod(String method) {
return HTTP_METHOD_POST.equalsIgnoreCase(method);
}
public static boolean isHttpHeadMethod(String method) {
return HTTP_METHOD_HEAD.equalsIgnoreCase(method);
}
/*
// Commented out to prevent pulling in ServletBase.
public static List<StringPair> getBoilerplateHeaders() {
return getBoilerplateHeaders(false);
}
public static List<StringPair> getBoilerplateHeaders(boolean isHeadRequest) {
String accept = isHeadRequest ? ACCEPT_FOR_HEAD : ACCEPT;
return ImmutableList.of(
new StringPair(HTTP_HEADER_ACCEPT, accept),
new StringPair(HTTP_HEADER_ACCEPT_CHARSET, ACCEPT_CHARSET),
new StringPair(HTTP_HEADER_ACCEPT_ENCODING, ACCEPT_ENCODING),
new StringPair(HTTP_HEADER_ACCEPT_LANGUAGE, ACCEPT_LANGUAGE),
new StringPair(HTTP_HEADER_DATE, ServletBase.httpDateString()));
}*/
/**
* Does the given HTTP status code indicate a valid response?
*
* @param status The status code to test.
* @return True only if it indicates a valid response.
*/
public static boolean isGoodHttpStatus(int status) {
return status == HttpServletResponse.SC_OK
|| status == HttpServletResponse.SC_PARTIAL_CONTENT;
}
public static URL urlFromString(String urlString) {
try {
return new URL(urlString);
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
public static URL urlFromString(URL baseUrl, String urlString) {
try {
return new URL(baseUrl, urlString);
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
public static URL urlFromParts(String protocol, String host, int port, String file) {
try {
return new URL(protocol, host, port, file);
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
public static URL parseUrlString(String urlString) {
try {
return new URL(urlString);
} catch (MalformedURLException e) {
return null;
}
}
public static URL parseUrlString(URL baseUrl, String urlString) {
try {
return new URL(baseUrl, urlString);
} catch (MalformedURLException e) {
return null;
}
}
public static URL parentUrl(URL url) {
String path = url.getPath();
int slash = path.lastIndexOf('/');
if (slash <= 0) {
return null;
}
return urlFromParts(url.getProtocol(), url.getHost(), url.getPort(), path.substring(0, slash));
}
public static URL stripQueryFromUrl(URL url) {
return mergeQueryIntoUrl(url, null);
}
public static URL mergeQueryIntoUrl(URL url, String query) {
return urlFromParts(url.getProtocol(), url.getHost(), url.getPort(),
newQuery(url.getPath(), query));
}
private static String newQuery(String path, String query) {
return Strings.isNullOrEmpty(query) ? path : path + "?" + query;
}
/**
* Converts a {@link URL} to a {@link URI}.
*
* @param url The URL to convert.
* @return The corresponding URI.
* @throws IllegalArgumentException if there are any parse errors in the
* conversion.
*/
public static URI toUri(URL url) {
return URI.create(url.toString());
}
/**
* Takes a given URI and returns a new one in which the query component has
* been replaced with a given query string.
*
* @param uri The base URI.
* @param query The new query component; may be {@code null} to delete the
* query component.
* @return A suitably modified URI.
*/
public static URI replaceUriQuery(URI uri, String query) {
try {
return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
uri.getPath(), query, uri.getFragment());
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Decodes an application/x-www-form-urlencoded format query string into its
* component parameters.
*
* @param uri A URI to decode the query string of.
* @return A multimap containing the decoded parameters from the uri.
* @throws IllegalArgumentException if the URI's query isn't correctly formatted.
*/
public static ListMultimap<String, String> decodeQueryString(URI uri) {
return decodeQueryString(uri.getQuery());
}
/**
* Decodes an application/x-www-form-urlencoded format query string into its
* component parameters.
*
* @param string The query string to decode.
* @return A multimap containing the decoded parameters from the string.
* @throws IllegalArgumentException if the string isn't correctly formatted.
*/
public static ListMultimap<String, String> decodeQueryString(String string) {
ListMultimap<String, String> result = ArrayListMultimap.create();
if (!Strings.isNullOrEmpty(string)) {
for (String element : QUERY_SPLITTER.split(string)) {
int index = element.indexOf('=');
if (index < 0) {
result.put(element, null);
} else {
result.put(element.substring(0, index), element.substring(index + 1));
}
}
}
return result;
}
/**
* Encodes a multimap of query parameters in application/x-www-form-urlencoded
* format.
*
* @param parameters The query parameters to be encoded.
* @return The encoded string.
*/
public static String encodeQueryString(Multimap<String, String> parameters) {
StringBuilder builder = new StringBuilder();
boolean needSeparator = false;
for (Map.Entry<String, String> entry : parameters.entries()) {
if (needSeparator) {
builder.append('&');
} else {
needSeparator = true;
}
builder.append(entry.getKey());
if (entry.getValue() != null) {
builder.append('=');
builder.append(entry.getValue());
}
}
return builder.toString();
}
private static final Splitter QUERY_SPLITTER = Splitter.on('&');
/**
* Gets the URI for an HTTP request.
*
* @param request The HTTP request to get the URI from.
* @param includeQuery If true, include the query part of the URI.
* @return The request URI.
* @throws IllegalArgumentException if the request's URI can't be parsed.
*/
public static URI getRequestUri(HttpServletRequest request, boolean includeQuery) {
URI uri = (request instanceof MockHttpServletRequest)
? getMockRequestUri((MockHttpServletRequest) request)
: URI.create(request.getRequestURL().toString());
return includeQuery
? uri
: replaceUriQuery(uri, null);
}
private static URI getMockRequestUri(MockHttpServletRequest request) {
// Note that it's not OK to call request.getRequestURL() because the mock
// implementation is broken and will include ":-1" if there's no port
// specified.
try {
return new URI(request.getScheme(), null, request.getServerName(), request.getServerPort(),
request.getRequestURI(), request.getQueryString(), null);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Gets the URL for an HTTP request.
*
* @param request The HTTP request to get the URL from.
* @param includeQuery If true, include the query part of the URL.
* @return The request URL.
* @throws IllegalArgumentException if the request's URL can't be parsed.
*/
public static URL getRequestUrl(HttpServletRequest request, boolean includeQuery) {
try {
return getRequestUri(request, includeQuery).toURL();
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Given a URL, gets a string suitable for logging. This string omits the URL
* query, as it might contain sensitive parameters that shouldn't be logged
* (e.g. a password). It also omits the fragment identifier, since that isn't
* usually needed in the log.
*
* @param url The URL to get a log string for.
* @return An appropriate string representation of the URL.
*/
public static String getUrlLogString(URL url) {
return getUriLogString(toUri(url));
}
/**
* Given a URL string, gets a string suitable for logging. This string omits
* the URL query, as it might contain sensitive parameters that shouldn't be
* logged (e.g. a password). It also omits the fragment identifier, since
* that isn't usually needed in the log.
*
* @param urlString The URL string to get a log string for.
* @return An appropriate string representation of the URL.
*/
public static String getUrlLogString(String urlString) {
URI uri;
try {
uri = new URI(urlString);
} catch (URISyntaxException e) {
// Dumb, but in the unlikely event we get the exception, it should serve.
int index = urlString.indexOf('?');
return (index >= 0)
? urlString.substring(0, index)
: urlString;
}
return getUriLogString(uri);
}
/**
* Given a URI, gets a string suitable for logging. This string omits the URI
* query, as it might contain sensitive parameters that shouldn't be logged
* (e.g. a password). It also omits the fragment identifier, since that isn't
* usually needed in the log.
*
* @param uri The URI to get a log string for.
* @return An appropriate string representation of the URL.
*/
public static String getUriLogString(URI uri) {
try {
return (new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), null, null))
.toASCIIString();
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Gets the header value strings for any headers matching a given name.
*
* @param name The header name to look for.
* @param request The request to look in.
* @return The header values as an immutable list.
*/
public static ImmutableList<String> getRequestHeaderValues(String name,
HttpServletRequest request) {
ImmutableList.Builder<String> builder = ImmutableList.builder();
Enumeration<?> e = request.getHeaders(name);
while (e.hasMoreElements()) {
builder.add(String.class.cast(e.nextElement()));
}
return builder.build();
}
/**
* Parse an HTTP header parameter. Parameters come in two forms:
*
* token PARAM_VALUE_SEPARATOR token
* token PARAM_VALUE_SEPARATOR quoted-string
*
* The character set for a "token" is restricted. A "quoted-string" is
* surrounded by double quotes and can contain nearly all characters, plus
* escaped characters.
*
* @param string The raw parameter string, assumed to have been trimmed of whitespace.
* @return A list of two strings, the name and the value.
* @throws IllegalArgumentException if the string can't be parsed.
*/
public static List<String> parseHttpParameter(String string) {
int equals = string.indexOf(PARAM_VALUE_SEPARATOR);
checkParameterArgument(equals >= 0, string);
String name = string.substring(0, equals);
checkParameterArgument(isHttpToken(name), string);
String rawValue = string.substring(equals + 1, string.length());
return ImmutableList.of(name,
isHttpToken(rawValue) ? rawValue : parseHttpQuotedString(rawValue));
}
private static void checkParameterArgument(boolean succeed, String argument) {
Preconditions.checkArgument(succeed, "Incorrectly formatted HTTP parameter: %s", argument);
}
/**
* Is the given string an HTTP token?
*
* @param string The string to test.
* @return True if the string is a valid HTTP token.
*/
public static boolean isHttpToken(String string) {
return !Strings.isNullOrEmpty(string) && TOKEN.matchesAllOf(string);
}
/**
* Is the given string something that can be encoded as an HTTP quoted-string?
*
* @param string The string to test.
* @return True if the string can be encoded using the quoted-string format.
*/
public static boolean isQuotedStringEncodable(String string) {
return string != null && TEXT.matchesAllOf(string);
}
/**
* Encodes a string so that it's suitable as an HTTP parameter value. In
* other words, if the string is an HTTP token, it's self encoding.
* Otherwise, it is converted to the quoted-string format.
*
* @param string The string to be encoded.
* @return The same string encoded as an HTTP parameter value.
* @throws IllegalArgumentException if the given string can't be encoded.
*/
public static String makeHttpParameterValueString(String string) {
if (isHttpToken(string)) {
return string;
}
StringBuilder builder = new StringBuilder();
writeQuotedString(string, builder);
return builder.toString();
}
/**
* Writes a string-valued HTTP parameter to a given string builder. The
* parameter is prefixed by {@link #PARAM_SEPARATOR}.
*
* @param name The parameter name, which must satisfy {@link #isHttpToken}.
* @param value The parameter value, which must satisfy
* {@link #isQuotedStringEncodable}.
* @param builder A string builder to write the parameter to.
* @throws IllegalArgumentException if {@code name} or {@code value} can't be
* encoded.
*/
public static void writeParameter(String name, String value, StringBuilder builder) {
writeParameterName(name, builder);
builder.append(PARAM_VALUE_SEPARATOR);
writeParameterValue(value, builder);
}
/**
* Writes a boolean-valued HTTP parameter to a given string builder. The
* parameter is prefixed by {@link #PARAM_SEPARATOR}.
*
* @param name The parameter name, which must satisfy {@link #isHttpToken}.
* @param value The parameter value.
* @param builder A string builder to write the parameter to.
* @throws IllegalArgumentException if {@code name} can't be encoded.
*/
public static void writeParameter(String name, boolean value, StringBuilder builder) {
if (value) {
writeParameterName(name, builder);
}
}
/**
* Writes an HTTP parameter name to a given string builder. The name is
* prefixed by {@link #PARAM_SEPARATOR}.
*
* @param name The parameter name, which must satisfy {@link #isHttpToken}.
* @param builder A string builder to write the name to.
* @throws IllegalArgumentException if {@code name} can't be encoded.
*/
public static void writeParameterName(String name, StringBuilder builder) {
Preconditions.checkArgument(isHttpToken(name));
builder.append(PARAM_SEPARATOR);
builder.append(name);
}
/**
* Writes an HTTP parameter value to a given string builder.
*
* @param value The parameter value, which must satisfy
* {@link #isQuotedStringEncodable}.
* @param builder A string builder to write the value to.
* @throws IllegalArgumentException if {@code value} can't be encoded.
*/
public static void writeParameterValue(String value, StringBuilder builder) {
if (isHttpToken(value)) {
builder.append(value);
} else {
writeQuotedString(value, builder);
}
}
/**
* Writes a string to a string builder in HTTP quoted-string format.
*
* @param string The string to be written.
* @param builder A string builder to write the string to.
* @throws IllegalArgumentException if {@code string} can't be encoded.
*/
public static void writeQuotedString(String string, StringBuilder builder) {
Preconditions.checkArgument(isQuotedStringEncodable(string),
"String can't be encoded as an HTTP parameter value: %s", string);
builder.append(STRING_DELIMITER);
for (char c : string.toCharArray()) {
if (c == STRING_QUOTE || c == STRING_DELIMITER) {
builder.append(STRING_QUOTE);
}
builder.append(c);
}
builder.append(STRING_DELIMITER);
}
/**
* Parses an HTTP quoted-string.
*
* @param string The string to parse.
* @return The parsed value of the quoted string.
* @throws IllegalArgumentException if the string isn't a valid quoted-string.
*/
public static String parseHttpQuotedString(String string) {
int end = string.length();
checkQuotedStringArgument(
(end >= 2
&& string.charAt(0) == STRING_DELIMITER
&& string.charAt(end - 1) == STRING_DELIMITER),
string);
StringBuilder builder = new StringBuilder();
boolean pendingQuote = false;
for (char c : string.substring(1, end - 1).toCharArray()) {
if (pendingQuote) {
pendingQuote = false;
checkQuotedStringArgument(CHAR.matches(c), string);
builder.append(c);
} else if (c == STRING_QUOTE) {
pendingQuote = true;
} else {
checkQuotedStringArgument(QDTEXT.matches(c), string);
builder.append(c);
}
}
checkQuotedStringArgument(!pendingQuote, string);
return builder.toString();
}
/**
* Gets the http range header value for requesting the first number of bytes.
*
* @param bytes The number of bytes to request.
* @return The range header value
*/
public static String getRangeString(int bytes) {
return String.format(RANGE_FORMAT, bytes);
}
private static void checkQuotedStringArgument(boolean succeed, String argument) {
Preconditions.checkArgument(succeed, "Incorrectly formatted quoted-string: %s", argument);
}
// These names are taken directly from RFC 2616.
private static final CharMatcher OCTET = CharMatcher.inRange('\u0000', '\u00ff');
// Not strictly correct: CHAR technically includes CR and LF, but only for
// line folding. Since we're looking at a post-line-folding string, they
// shouldn't be present.
private static final CharMatcher CHAR = difference(CharMatcher.ASCII, CharMatcher.anyOf("\n\r"));
/** ASCII control characters. */
public static final CharMatcher CTLS =
CharMatcher.inRange('\u0000', '\u001f').or(CharMatcher.is('\u007f'));
/** ASCII alphabetic characters. */
public static final CharMatcher ALPHA =
CharMatcher.anyOf("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
/** ASCII digit characters. */
public static final CharMatcher DIGIT = CharMatcher.anyOf("0123456789");
/** Linear white space. */
public static final CharMatcher LWS = CharMatcher.anyOf(" \t");
/** Plain text. */
public static final CharMatcher TEXT = union(difference(OCTET, CTLS), LWS);
// Text that can be included in a quoted-string without backquotes. Note that
// RFC 2616 specifies only '"' as an exception, but clearly '\\' needs to be
// excepted as well.
private static final CharMatcher QDTEXT = difference(TEXT, CharMatcher.anyOf("\"\\"));
// Separator characters that aren't allowed in most places except inside
// quoted-strings.
private static final CharMatcher SEPARATORS = CharMatcher.anyOf("()<>@,;:\\\"/[]?={} \t");
// The constituent characters of a token.
private static final CharMatcher TOKEN = difference(CharMatcher.ASCII, union(CTLS, SEPARATORS));
private static CharMatcher union(CharMatcher m1, CharMatcher m2) {
return m1.or(m2);
}
private static CharMatcher difference(CharMatcher m1, CharMatcher m2) {
return m1.and(m2.negate());
}
// HTTP date formats (from RFC 2616):
//
// HTTP-date = rfc1123-date | rfc850-date | asctime-date
// rfc1123-date = wkday "," SP date1 SP time SP "GMT"
// rfc850-date = weekday "," SP date2 SP time SP "GMT"
// asctime-date = wkday SP date3 SP time SP 4DIGIT
// date1 = 2DIGIT SP month SP 4DIGIT
// ; day month year (e.g., 02 Jun 1982)
// date2 = 2DIGIT "-" month "-" 2DIGIT
// ; day-month-year (e.g., 02-Jun-82)
// date3 = month SP ( 2DIGIT | ( SP 1DIGIT ))
// ; month day (e.g., Jun 2)
// time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
// ; 00:00:00 - 23:59:59
// wkday = "Mon" | "Tue" | "Wed"
// | "Thu" | "Fri" | "Sat" | "Sun"
// weekday = "Monday" | "Tuesday" | "Wednesday"
// | "Thursday" | "Friday" | "Saturday" | "Sunday"
// month = "Jan" | "Feb" | "Mar" | "Apr"
// | "May" | "Jun" | "Jul" | "Aug"
// | "Sep" | "Oct" | "Nov" | "Dec"
private static final String DATE_FORMAT_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
private static final String DATE_FORMAT_RFC850 = "EEEE, dd-MMM-yy HH:mm:ss zzz";
private static final String DATE_FORMAT_ASCTIME = "EEE MMM dd HH:mm:ss yyyy";
/**
* Generates an HTTP date string.
*
* @param date A date value specified as a non-negative difference from the
* epoch in milliseconds.
* @return An HTTP date string representing that date.
*/
public static String generateHttpDate(long date) {
return getDateFormat(DATE_FORMAT_RFC1123).format(new Date(date));
}
/**
* Parses an HTTP date string.
*
* @param dateString The string to parse.
* @return The difference, measured in milliseconds, between the specified
* date and 1970-01-01T00:00:00Z.
* @throws IllegalArgumentException if the date string can't be parsed.
*/
public static long parseHttpDate(String dateString) {
try {
return parseDate(DATE_FORMAT_RFC1123, dateString);
} catch (ParseException e) {
// Fall through to next format.
}
try {
return parseDate(DATE_FORMAT_RFC850, dateString);
} catch (ParseException e) {
// Fall through to next format.
}
try {
return parseDate(DATE_FORMAT_ASCTIME, dateString);
} catch (ParseException e) {
throw new IllegalArgumentException("Can't parse as HTTP date string: " + dateString);
}
}
private static long parseDate(String formatString, String dateString)
throws ParseException {
return getDateFormat(formatString).parse(dateString).getTime();
}
private static DateFormat getDateFormat(String formatString) {
DateFormat format = new SimpleDateFormat(formatString);
format.setCalendar(Calendar.getInstance(GMT, Locale.US));
return format;
}
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
/**
* Is the given string a valid domain name? Uses a fairly restrictive
* definition, corresponding to the "preferred syntax" of RFC 1034 as updated
* by RFC 1123.
*
* @param string The string to be tested.
* @return True only if the string is a valid domain name.
*/
public static boolean isValidDomainName(String string) {
return parseDomainName(string) != null;
}
/**
* Converts a given domain name to its canonical form. This should eventually
* handle IDNA names, but for now we just canonicalize case.
*
* @param domainName The domain name to convert.
* @return The canonical form for {@code domainName}.
* @throws IllegalArgumentException if {@code domainName} doesn't satisfy
* {@code #isValidDomainName}.
*/
public static String canonicalizeDomainName(String domainName) {
List<String> labels = parseDomainName(domainName);
Preconditions.checkArgument(labels != null, "Not a valid domain name: %s", domainName);
return labelsToDomanName(labels);
}
/**
* Gets the "parent domain" name of a domain name.
*
* @param domainName The domain name to get the parent domain name of.
* @return The parent domain name, or {@code null} if there isn't one.
* @throws IllegalArgumentException if {@code domainName} doesn't satisfy
* {@code #isValidDomainName}.
*/
public static String domainNameParent(String domainName) {
List<String> labels = parseDomainName(domainName);
Preconditions.checkArgument(labels != null, "Not a valid domain name: %s", domainName);
if (labels.size() < 2) {
return null;
}
labels.remove(0);
return labelsToDomanName(labels);
}
private static List<String> parseDomainName(String domainName) {
if (!(domainName.length() >= 1 && domainName.length() <= 255)) {
return null;
}
List<String> labels = Lists.newArrayList(DOMAIN_NAME_SPLITTER.split(domainName));
if (!(labels.size() >= 1 && labels.size() <= 127)) {
return null;
}
for (String label : labels) {
if (!isValidDomainLabel(label)) {
return null;
}
}
// Eliminates IPv4 addresses:
if (DIGIT.matchesAllOf(labels.get(labels.size() - 1))) {
return null;
}
return labels;
}
private static boolean isValidDomainLabel(String label) {
return label.length() >= 1
&& label.length() <= 63
&& DOMAIN_LABEL_CHAR.matchesAllOf(label)
&& label.charAt(0) != '-'
&& label.charAt(label.length() - 1) != '-';
}
private static String labelsToDomanName(List<String> labels) {
return DOMAIN_NAME_JOINER.join(
Iterables.transform(labels,
new Function<String, String>() {
@Override
public String apply(String label) {
return label.toLowerCase(Locale.US);
}
}));
}
private static final CharMatcher DOMAIN_LABEL_CHAR = ALPHA.or(DIGIT).or(CharMatcher.is('-'));
private static final Splitter DOMAIN_NAME_SPLITTER = Splitter.on('.');
private static final Joiner DOMAIN_NAME_JOINER = Joiner.on('.');
}