IuWebUtils.java
/*
* Copyright © 2024 Indiana University
* All rights reserved.
*
* BSD 3-Clause License
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* - Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package edu.iu;
import java.net.InetAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Queue;
/**
* Provides useful utility methods for low-level web client and server
* interactions.
*/
public final class IuWebUtils {
private static final Map<String, InetAddress> IP_CACHE = new IuCacheMap<>(Duration.ofSeconds(5L));
private static char DQUOTE = 0x22;
private static char HTAB = 0x09;
private static char SP = 0x20;
/**
* VCHAR = %x21-7E
*
* @param c character
* @return true if c matches VCHAR ABNF rule; else false
* @see <a href=
* "https://datatracker.ietf.org/doc/html/rfc5234#appendix-B">RFC-5234 Core
* ABNF</a>
*/
static boolean vchar(char c) {
return c >= 0x21 //
&& c <= 0x7e;
}
/**
* ALPHA = %x41-5A / %x61-7A
*
* @param c character
* @return true if c matches ALHPA ABNF rule; else false
* @see <a href=
* "https://datatracker.ietf.org/doc/html/rfc5234#appendix-B">RFC-5234 Core
* ABNF</a>
*/
static boolean alpha(char c) {
return (c >= 0x41 //
&& c <= 0x5a) //
|| (c >= 0x61 //
&& c <= 0x7a);
}
/**
* DIGIT = %x30-39
*
* @param c character
* @return true if c matches DIGIT ABNF rule; else false
* @see <a href=
* "https://datatracker.ietf.org/doc/html/rfc5234#appendix-B">RFC-5234 Core
* ABNF</a>
*/
static boolean digit(char c) {
return c >= 0x30 //
&& c <= 0x39;
}
/**
* ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
*
* @param c character
* @return true if c matches ctext ABNF rule; else false
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static boolean ctext(char c) {
return c == HTAB //
|| c == SP //
|| (c >= 0x21 // '!'-'''
&& c <= 0x27) //
|| (c >= 0x2A // '*'-'['
&& c <= 0x5b) //
|| (c >= 0x5d // ']'-'~'
&& c <= 0x7e) //
|| obsText(c);
}
/**
* obs-text = %x80-FF
*
* @param c character
* @return true if c matches obs-text ABNF rule; else false
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static boolean obsText(char c) {
return c >= 0x80 //
&& c <= 0xff;
}
/**
* comment = "(" *( ctext / quoted-pair / comment ) ")"
*
* @param s input string
* @param pos position at start of comment
* @return end position after matching comment ABNF rule; returns pos if token68
* was not matched
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static int comment(String s, final int pos) {
if (s.charAt(pos) != '(')
return pos;
var p = pos + 1;
int n;
char c;
do {
c = s.charAt(p);
if (ctext(c))
p++;
else if ((n = quotedPair(s, p)) > p //
|| (n = comment(s, p)) > p)
p = n;
else if (c == ')')
return p + 1;
else
return pos;
} while (p < s.length());
return pos;
}
/**
* token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
*
* @param s input string
* @param pos position at start of token68 character
* @return end position after matching token68 ABNF rule; returns pos if token68
* was not matched
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static int token68(String s, int pos) {
if (pos >= s.length())
return pos;
char c;
do {
if (pos == s.length())
return pos;
c = s.charAt(pos++);
} while (alpha(c) //
|| digit(c) //
|| "-._~+/".indexOf(c) != -1);
pos--;
do {
if (pos == s.length())
return pos;
c = s.charAt(pos++);
} while (c == '=');
return pos - 1;
}
/**
* tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" /
* "_" / "`" / "|" / "~" / DIGIT / ALPHA
*
* @param c character
* @return true if c matches obs-text ABNF rule; else false
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static boolean tchar(char c) {
return alpha(c) //
|| digit(c) //
|| "!#$%&'*+-.^_`!~".indexOf(c) != -1;
}
/**
* token = 1*tchar
*
* @param s input string
* @param pos position at start of token character
* @return end position of a matching token ABNF rule; returns pos if token was
* not matched
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static int token(String s, int pos) {
if (pos >= s.length())
return pos;
char c;
do {
c = s.charAt(pos++);
if (pos == s.length() //
&& tchar(c))
return pos;
} while (tchar(c));
return pos - 1;
}
/**
* BWS = OWS
*
* <p>
* OWS = *( SP / HTAB )
* </p>
*
* <p>
* RWS = 1*( SP / HTAB )
* </p>
*
* @param s input string
* @param pos position at start of token character
* @return end position of a matching BWS ABNF rule
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static int bws(String s, int pos) {
if (pos >= s.length())
return pos;
char c;
do {
c = s.charAt(pos++);
if (pos == s.length() //
&& (c == SP //
|| c == HTAB))
return pos;
} while (c == SP //
|| c == HTAB);
return pos - 1;
}
/**
* 1*SP
*
* @param s input string
* @param pos position at start of token character
* @return end position of a matching sp ABNF rule
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static int sp(String s, int pos) {
if (pos >= s.length())
return pos;
char c;
do {
c = s.charAt(pos++);
if (pos == s.length() //
&& (c == SP))
return pos;
} while (c == SP);
return pos - 1;
}
/**
* product = token [ "/" product-version ]
* <p>
* product-version = token
* </p>
*
* @param s input string
* @param pos position at start of product expression
* @return end position of a matching product ABNF rule; pos if not matched
*/
static int product(String s, int pos) {
final var token = token(s, pos);
if (token == pos //
|| s.length() == token //
|| s.charAt(token) != '/')
return token;
final var version = token(s, token + 1);
if (version == token + 1)
return pos;
else
return version;
}
/**
* qdtext = HTAB / SP / "!" / %x23-5B / %x5D-7E / obs-text
*
* @param c character
* @return true if c matches qdtext ABNF rule; else false
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static boolean qdtext(char c) {
return c == HTAB //
|| c == SP //
|| c == '!' //
|| (c >= 0x23 //
&& c <= 0x5b) //
|| (c >= 0x5d //
&& c <= 0x7e) //
|| obsText(c);
}
/**
* quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
*
* @param s input string
* @param pos position at start of token character
* @return end position of a matching quoted-pair ABNF rule; pos if the rule was
* not matched
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static int quotedPair(String s, int pos) {
char c;
if (pos < s.length() - 1 //
&& s.charAt(pos) == '\\' //
&& ((c = s.charAt(pos + 1)) == HTAB //
|| c == SP //
|| vchar(c) //
|| obsText(c)))
pos += 2;
return pos;
}
/**
* quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
*
* @param s input string
* @param start position at start of token character
* @return end position of a matching BWS ABNF rule
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* HTTP Semantics Collected ABNF</a>
*/
static int quotedString(String s, int start) {
if (start >= s.length() - 1 //
|| s.charAt(start) != DQUOTE)
return start;
var pos = start + 1;
do {
final var qp = quotedPair(s, pos);
if (qp != pos)
pos = qp;
if (pos >= s.length())
return start;
} while (qdtext(s.charAt(pos++)));
if (s.charAt(pos - 1) != DQUOTE)
return start;
else
return pos;
}
/**
* Determines whether or not a string is composed entirely of non-whitespace
* visible ASCII characters, and at least minLength characters, but no longer
* than 1024.
*
* @param s string to check
* @param minLength minimum length
* @return true if all characters are visible ASCII
*/
public static boolean isVisibleAscii(String s, int minLength) {
final var len = s.length();
if (len < minLength //
|| len > 1024)
return false;
for (var i = 0; i < len; i++)
if (!vchar(s.charAt(i)))
return false;
return true;
}
/**
* Determines if a root {@link URI} encompasses a resource {@link URI}.
*
* @param rootUri root {@link URI}
* @param resourceUri resource {@link URI}
* @return {@link URI}
*/
public static boolean isRootOf(URI rootUri, URI resourceUri) {
if (rootUri.equals(resourceUri))
return true;
if (!resourceUri.isAbsolute() //
|| resourceUri.isOpaque() //
|| !IuObject.equals(rootUri.getScheme(), resourceUri.getScheme()) //
|| !IuObject.equals(rootUri.getAuthority(), resourceUri.getAuthority()))
return false;
final var root = rootUri.getPath();
if (root.isEmpty())
return true;
final var resource = resourceUri.getPath();
final var l = root.length();
return resource.startsWith(root) //
&& (root.charAt(l - 1) == '/' //
|| resource.charAt(l) == '/');
}
/**
* Creates an authentication challenge sending to a client via the
* <strong>WWW-Authenticate</strong> header.
*
* @param scheme authentication scheme to request
* @param params challenge attributes for informing the client of how to
* authenticate
* @return authentication challenge
* @see <a href="https://datatracker.ietf.org/doc/html/rfc9110">RFC-9110 Section
* 11.1</a>
*/
public static String createChallenge(String scheme, Map<String, String> params) {
if (!params.containsKey("realm"))
throw new IllegalArgumentException("missing realm");
final var sb = new StringBuilder();
// auth-scheme = token
if (token(scheme, 0) != scheme.length())
throw new IllegalArgumentException("invalid auth scheme");
sb.append(scheme);
sb.append(' ');
var first = true;
for (final var paramEntry : params.entrySet()) {
// auth-param = token BWS "=" BWS ( token / quoted-string )
final var key = paramEntry.getKey();
final var value = paramEntry.getValue();
if (token(key, 0) == key.length()) {
for (int i = 0; i < value.length(); i++) {
final var c = value.charAt(i);
if (c < SP //
&& c != HTAB)
throw new IllegalArgumentException("invalid parameter value");
}
} else if (token68(key, 0) == key.length()) {
if (!value.isEmpty())
throw new IllegalArgumentException("invalid encoded parameter");
} else
throw new IllegalArgumentException("invalid auth param");
if (first)
first = false;
else
sb.append(" ");
sb.append(key);
if (!value.isEmpty())
sb.append("=\"") //
.append(paramEntry.getValue() //
.replace("\\", "\\\\") //
.replace("\"", "\\\"")) //
.append("\"");
}
return sb.toString();
}
/**
* Parses challenge parameters from a <strong>WWW-Authenticate</strong> header.
*
* @param wwwAuthenticate WWW-Authenticate header challenge value
* @return Parsed authentication challenge parameters
*/
public static Iterator<IuWebAuthenticationChallenge> parseAuthenticateHeader(String wwwAuthenticate) {
// WWW-Authenticate = [ challenge *( OWS "," OWS challenge ) ]
return new Iterator<IuWebAuthenticationChallenge>() {
private int pos = 0;
@Override
public boolean hasNext() {
return pos < wwwAuthenticate.length();
}
@Override
public IuWebAuthenticationChallenge next() {
// challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
// auth-scheme = token
final var endOfAuthScheme = token(wwwAuthenticate, pos);
if (endOfAuthScheme == pos)
throw new IllegalArgumentException("invalid auth-scheme at " + pos);
final var authScheme = wwwAuthenticate.substring(pos, endOfAuthScheme);
pos = sp(wwwAuthenticate, endOfAuthScheme);
final Map<String, String> params = new LinkedHashMap<>();
while (hasNext() && wwwAuthenticate.charAt(pos) != ',') {
// auth-param = token BWS "=" BWS ( token / quoted-string )
var endOfToken = token(wwwAuthenticate, pos);
var eqOrStartOfNextToken = bws(wwwAuthenticate, endOfToken);
if (endOfToken > pos //
&& endOfToken < wwwAuthenticate.length() //
&& eqOrStartOfNextToken < wwwAuthenticate.length() //
&& wwwAuthenticate.charAt(eqOrStartOfNextToken) == '=') {
final var name = wwwAuthenticate.substring(pos, endOfToken);
pos = bws(wwwAuthenticate, eqOrStartOfNextToken + 1);
var endOfValue = quotedString(wwwAuthenticate, pos);
if (endOfValue == pos) {
endOfValue = token(wwwAuthenticate, pos);
if (endOfValue == pos)
throw new IllegalArgumentException("expected quoted-string at " + pos);
else
params.put(name, wwwAuthenticate.substring(pos, endOfValue));
} else
params.put(name, wwwAuthenticate.substring(pos + 1, endOfValue - 1) //
.replace("\\\\", "\\") //
.replace("\\\"", "\""));
pos = sp(wwwAuthenticate, endOfValue);
} else {
endOfToken = token68(wwwAuthenticate, pos);
if (endOfToken == pos)
throw new IllegalArgumentException("invalid auth-param at " + pos);
else if (endOfToken < wwwAuthenticate.length())
throw new IllegalArgumentException("expected SP at " + pos);
params.put(wwwAuthenticate.substring(pos, endOfToken), "");
pos = sp(wwwAuthenticate, endOfToken);
}
final var bws = bws(wwwAuthenticate, pos);
if (bws < wwwAuthenticate.length() //
&& wwwAuthenticate.charAt(bws) == ',')
pos = bws;
}
final var realm = params.remove("realm");
if (hasNext())
pos = bws(wwwAuthenticate, pos + 1);
return new IuWebAuthenticationChallenge() {
@Override
public String getAuthScheme() {
return authScheme;
}
@Override
public String getRealm() {
return realm;
}
@Override
public Map<String, String> getParameters() {
return params;
}
};
}
};
}
/**
* Validates a user-agent header value.
*
* <p>
* User-Agent = product *( RWS ( product / comment ) )
* </p>
*
* @param userAgent user-agent header value
* @see <a href=
* "https://www.rfc-editor.org/rfc/rfc9110.html#name-collected-abnf">RFC-9110
* Appendix A</a>
*/
public static void validateUserAgent(String userAgent) {
var pos = product(userAgent, 0);
if (pos <= 0)
throw new IllegalArgumentException();
while (pos < userAgent.length()) {
var n = bws(userAgent, pos);
if (n <= pos)
throw new IllegalArgumentException();
else
pos++;
if ((n = product(userAgent, pos)) <= pos //
&& (n = comment(userAgent, pos)) <= pos)
throw new IllegalArgumentException();
else
pos = n;
}
}
/**
* Parses a query string.
*
* @param queryString query string
* @return {@link Map}
*/
public static Map<String, ? extends Iterable<String>> parseQueryString(String queryString) {
final Map<String, Queue<String>> parsedParameterValues = new LinkedHashMap<>();
var startOfName = queryString.startsWith("?") ? 1 : 0;
if (startOfName == queryString.length())
return parsedParameterValues;
var endOfName = queryString.indexOf('=', startOfName);
var endOfValue = queryString.indexOf('&', startOfName);
int startOfValue;
if (endOfValue == -1)
endOfValue = queryString.length();
if (endOfName == -1 //
|| endOfName > endOfValue)
endOfName = startOfValue = endOfValue;
else
startOfValue = endOfName + 1;
while (true) {
final var name = queryString.substring(startOfName, endOfName);
var values = parsedParameterValues.get(name);
if (values == null)
parsedParameterValues.put(name, values = new ArrayDeque<>());
if (endOfValue == queryString.length()) {
final var value = queryString.substring(startOfValue);
values.offer(IuException.unchecked(() -> URLDecoder.decode(value, "UTF-8")));
endOfName = -1;
break;
}
final var value = queryString.substring(startOfValue, endOfValue);
values.offer(IuException.unchecked(() -> URLDecoder.decode(value, "UTF-8")));
startOfName = endOfValue + 1;
endOfName = queryString.indexOf('=', startOfName);
endOfValue = queryString.indexOf('&', startOfName);
if (endOfValue == -1)
endOfValue = queryString.length();
if (endOfName == -1 //
|| endOfName > endOfValue)
endOfName = startOfValue = endOfValue;
else
startOfValue = endOfName + 1;
}
return parsedParameterValues;
}
/**
* Creates a query string from a map.
*
* @param params {@link Map} of parameter values
* @return query string
*/
public static String createQueryString(Map<String, ? extends Iterable<String>> params) {
final var queryString = new StringBuilder();
for (final var paramEntry : params.entrySet())
for (final var paramValue : paramEntry.getValue()) {
if (queryString.length() > 0)
queryString.append('&');
queryString.append(IuException.unchecked(() -> URLEncoder.encode(paramEntry.getKey(), "UTF-8")));
queryString.append("=").append(IuException.unchecked(() -> URLEncoder.encode(paramValue, "UTF-8")));
}
return queryString.toString();
}
/**
* Parses a header value composed of key/value pairs separated by semicolon ';'.
*
* @param headerValue header value
* @return {@link Map} of header elements
*/
public static Map<String, String> parseHeader(String headerValue) {
int semicolon = headerValue.indexOf(';');
if (semicolon == -1)
return Collections.singletonMap("", headerValue);
final Map<String, String> parsedHeader = new LinkedHashMap<>();
parsedHeader.put("", headerValue.substring(0, semicolon));
while (semicolon < headerValue.length()) {
final var start = semicolon + 1;
final var eq = headerValue.indexOf('=', start + 1);
semicolon = headerValue.indexOf(';', start + 1);
if (semicolon == -1)
semicolon = headerValue.length();
if (eq == -1 || eq > semicolon)
parsedHeader.put(headerValue.substring(start, semicolon).trim(), "");
else {
final var elementName = headerValue.substring(start, eq).trim();
final String elementValue = headerValue.substring(eq + 1, semicolon).trim();
parsedHeader.put(elementName, elementValue);
}
}
return parsedHeader;
}
/**
* Validates and normalizes case for an HTTP header name.
*
* <p>
* Follows each hyphen '-' character with an upper case character; converts
* other characters {@link Character#toLowerCase(char) to lower case}
* </p>
*
* @param headerName HTTP header name
* @return {@link String}
* @throws IllegalArgumentException If the name contains non-alphabetic
* characters other than hyphen '-', or if the
* name begins or ends with a hyphen.
*/
public static String normalizeHeaderName(String headerName) throws IllegalArgumentException {
if (!headerName.matches("\\p{Alpha}+(\\-\\p{Alpha}+?)*"))
throw new IllegalArgumentException("Invalid header name " + headerName);
final var sb = new StringBuilder(headerName.toLowerCase());
sb.setCharAt(0, Character.toUpperCase(sb.charAt(0)));
for (int i = headerName.indexOf('-') + 1; //
i != 0; //
i = headerName.indexOf('-', i + 1) + 1)
sb.setCharAt(i, Character.toUpperCase(sb.charAt(i)));
return sb.toString();
}
/**
* Describes an HTTP status code.
*
* @param statusCode HTTP status code
* @return {@link String}
*/
public static String describeStatus(int statusCode) {
switch (statusCode) {
case 100:
return statusCode + " CONTINUE";
case 101:
return statusCode + " SWITCHING PROTOCOLS";
case 200:
return statusCode + " OK";
case 201:
return statusCode + " CREATED";
case 202:
return statusCode + " ACCEPTED";
case 203:
return statusCode + " NON AUTHORITATIVE INFORMATION";
case 204:
return statusCode + " NO CONTENT";
case 205:
return statusCode + " RESET CONTENT";
case 206:
return statusCode + " PARTIAL CONTENT";
case 300:
return statusCode + " MULTIPLE CHOICES";
case 301:
return statusCode + " MOVED PERMANENTLY";
case 302:
return statusCode + " FOUND";
case 303:
return statusCode + " SEE OTHER";
case 304:
return statusCode + " NOT MODIFIED";
case 305:
return statusCode + " USE PROXY";
case 307:
return statusCode + " TEMPORARY REDIRECT";
case 400:
return statusCode + " BAD REQUEST";
case 401:
return statusCode + " UNAUTHORIZED";
case 402:
return statusCode + " PAYMENT REQUIRED";
case 403:
return statusCode + " FORBIDDEN";
case 404:
return statusCode + " NOT FOUND";
case 405:
return statusCode + " METHOD NOT ALLOWED";
case 406:
return statusCode + " NOT ACCEPTABLE";
case 407:
return statusCode + " PROXY AUTHENTICATION REQUIRED";
case 408:
return statusCode + " REQUEST TIMEOUT";
case 409:
return statusCode + " CONFLICT";
case 410:
return statusCode + " GONE";
case 411:
return statusCode + " LENGTH REQUIRED";
case 412:
return statusCode + " PRECONDITION FAILED";
case 413:
return statusCode + " REQUEST ENTITY TOO LARGE";
case 414:
return statusCode + " REQUEST URI TOO LONG";
case 415:
return statusCode + " UNSUPPORTED MEDIA TYPE";
case 416:
return statusCode + " REQUESTED RANGE NOT SATISFIABLE";
case 417:
return statusCode + " EXPECTATION FAILED";
case 500:
return statusCode + " INTERNAL SERVER ERROR";
case 501:
return statusCode + " NOT IMPLEMENTED";
case 502:
return statusCode + " BAD GATEWAY";
case 503:
return statusCode + " SERVICE UNAVAILABLE";
case 504:
return statusCode + " GATEWAY TIMEOUT";
case 505:
return statusCode + " HTTP VERSION NOT SUPPORTED";
default:
return statusCode + " UNKNOWN";
}
}
/**
* Resolves and caches the {@link InetAddress IP address} for a host name.
*
* @param hostname host name
* @return resolved {@link InetAddress}
*/
public static InetAddress getInetAddress(String hostname) {
var addr = IP_CACHE.get(hostname);
if (addr != null)
return addr;
addr = IuException.unchecked(() -> InetAddress.getByName(hostname));
IP_CACHE.put(hostname, addr);
return addr;
}
/**
* Determines whether or not an IP address is included in a CIDR range.
*
* @param address address
* @param range CIDR range
* @return true if the range includes the address; else false
*/
public static boolean isInetAddressInRange(InetAddress address, String range) {
byte[] hostaddr = address.getAddress();
int lastSlash = range.lastIndexOf('/');
byte[] rangeaddr;
int maskbits;
if (lastSlash == -1) {
rangeaddr = getInetAddress(range).getAddress();
maskbits = rangeaddr.length * 8;
} else {
rangeaddr = getInetAddress(range.substring(0, lastSlash)).getAddress();
maskbits = Integer.parseInt(range.substring(lastSlash + 1));
}
for (int i = 0; i < rangeaddr.length; i++) {
if (maskbits >= 8) {
maskbits -= 8;
if (hostaddr[i] != rangeaddr[i])
return false;
} else if (maskbits > 0) {
int mask = ~((1 << (8 - maskbits)) - 1);
return (hostaddr[i] & mask) == (rangeaddr[i] & mask);
} else
break;
}
return true;
}
private IuWebUtils() {
}
}