/*
* Copyright (C) 2015 SoftIndex LLC.
*
* 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 io.datakernel.http;
import io.datakernel.annotation.Nullable;
import io.datakernel.exception.ParseException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.LinkedHashMap;
import java.util.Map;
import static io.datakernel.bytebuf.ByteBufStrings.decodeDecimal;
import static io.datakernel.bytebuf.ByteBufStrings.encodeAscii;
import static io.datakernel.util.StringUtils.splitToList;
/**
* Util for working with {@link HttpRequest}
*/
public final class HttpUtils {
private static final char COMMA_SEPARATOR = ',';
private static final char QUERY_SEPARATOR = '&';
private static final String ENCODING = "UTF-8";
public static InetAddress inetAddress(String host) {
try {
return InetAddress.getByName(host);
} catch (UnknownHostException e) {
throw new IllegalArgumentException(e);
}
}
// https://url.spec.whatwg.org/
public static boolean isInetAddress(String host) {
int colons = 0;
int dots = 0;
byte[] bytes = encodeAscii(host);
// expect ipv6 address
if (bytes[0] == '[') {
return bytes[bytes.length - 1] == ']' && checkIpv6(bytes, 1, bytes.length - 1);
}
// assume ipv4 could be as oct, bin, dec; ipv6 - hex
for (byte b : bytes) {
if (b == '.') {
dots++;
} else if (b == ':') {
if (dots != 0) {
return false;
}
colons++;
} else if (Character.digit(b, 16) == -1) {
return false;
}
}
if (dots < 4) {
if (colons > 0 && colons < 8) {
return checkIpv6(bytes, 0, bytes.length);
}
return checkIpv4(bytes, 0, bytes.length);
}
return false;
}
/*
* Checks only dot decimal format(192.168.0.208 for example)
* more -> https://en.wikipedia.org/wiki/IPv4
*/
private static boolean checkIpv4(byte[] bytes, int pos, int length) {
int start = pos;
for (int i = pos; i < length; i++) {
// assume at least one more symbol is present after dot
if (i == length - 1 && bytes[i] == '.') {
return false;
}
if (bytes[i] == '.' || i == length - 1) {
int v;
if (i - start == 0 && i != length - 1) {
return false;
}
try {
v = decodeDecimal(bytes, start, i - start);
} catch (ParseException e) {
return false;
}
if (v < 0 || v > 255) return false;
start = i + 1;
}
}
return true;
}
/*
* http://stackoverflow.com/questions/5963199/ipv6-validation
* rfc4291
*
* IPV6 addresses are represented as 8, 4 hex digit groups of numbers
* 2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d
*
* leading zeros are not necessary, however at least one digit should be present
*
* the null group ':0000:0000:0000'(one or more) could be substituted with '::' once per address
*
* x:x:x:x:x:x:d.d.d.d - 6 ipv6 + 4 ipv4
* ::d.d.d.d
* */
private static boolean checkIpv6(byte[] bytes, int pos, int length) {
boolean shortHand = false; // denotes usage of ::
int numCount = 0;
int blocksCount = 0;
int start = 0;
while (pos < length) {
if (bytes[pos] == ':') {
start = pos;
blocksCount++;
numCount = 0;
if (pos > 0 && bytes[pos - 1] == ':') {
if (shortHand) return false;
else {
shortHand = true;
}
}
} else if (bytes[pos] == '.') {
return checkIpv4(bytes, start + 1, length - start + 1);
} else {
if (Character.digit(bytes[pos], 16) == -1) {
return false;
}
numCount++;
if (numCount > 4) {
return false;
}
}
pos++;
}
return blocksCount > 6 || shortHand;
}
static int skipSpaces(byte[] bytes, int pos, int end) {
while (pos < end && bytes[pos] == ' ') {
pos++;
}
return pos;
}
static int parseQ(byte[] bytes, int pos, int length) throws ParseException {
if (bytes[pos] == '1') {
return 100;
} else {
length = length > 4 ? 2 : length - 2;
int q = decodeDecimal(bytes, pos + 2, length);
if (length == 1) q *= 10;
return q;
}
}
/**
* Returns a real IP of client which send this request, if it has header X_FORWARDED_FOR
*
* @param request received request
*/
public static InetAddress getRealIp(HttpRequest request) {
String s = request.getHeader(HttpHeaders.X_FORWARDED_FOR);
if (!isNullOrEmpty(s)) {
String clientIP = splitToList(COMMA_SEPARATOR, s).iterator().next().trim();
try {
return HttpUtils.inetAddress(clientIP);
} catch (Exception ignored) {
}
}
return request.getRemoteAddress();
}
public static InetAddress getRealIpNginx(HttpRequest request) {
String s = request.getHeader(HttpHeaders.X_REAL_IP);
if (!isNullOrEmpty(s)) {
try {
return InetAddress.getByName(s.trim());
} catch (Exception ignored) {
}
}
return getRealIp(request);
}
/**
* Returns the host of Http request
*
* @param request Http request with header host
*/
@Nullable
public static String getHost(HttpRequest request) {
String host = request.getHeader(HttpHeaders.HOST);
if ((host == null) || host.isEmpty())
return null;
return host;
}
/**
* Returns the URL from Http Request
*
* @param request Http request with URL
*/
@Nullable
public static String getFullUrl(HttpRequest request) {
HttpUri url = request.getUrl();
if (!url.isPartial()) {
return url.toString();
}
String host = getHost(request);
if (host == null) {
return null;
}
return "http://" + host + url.getPathAndQuery();
}
/**
* Method which parses string with URL of query, and returns collection with keys - name of
* parameter, value - value of it. Using encoding UTF-8
*
* @param query string with URL for parsing
* @return collection with keys - name of parameter, value - value of it.
*/
public static Map<String, String> extractParameters(String query) {
return extractParameters(query, ENCODING);
}
/**
* Method which parses string with URL of query, and returns collection with keys - name of
* parameter, value - value of it.
*
* @param query string with URL for parsing
* @param enc encoding of this string
* @return collection with keys - name of parameter, value - value of it.
*/
public static Map<String, String> extractParameters(String query, String enc) {
LinkedHashMap<String, String> qps = new LinkedHashMap<>();
for (String pair : splitToList(QUERY_SEPARATOR, query)) {
pair = pair.trim();
int pos = pair.indexOf('=');
String name;
String val = null;
if (pos < 0)
name = pair;
else {
name = pos == 0 ? "" : pair.substring(0, pos);
++pos;
val = pos < pair.length() ? pair.substring(pos) : "";
}
try {
name = decode(name, enc);
if (val != null) {
val = decode(val, enc);
qps.put(name, val);
}
} catch (ParseException ignored) {
}
}
return qps;
}
/**
* Method which creates string with parameters and its value in format URL. Using encoding UTF-8
*
* @param q map in which keys if name of parameters, value - value of parameters.
* @return string with parameters and its value in format URL
*/
public static String urlQueryString(Map<String, String> q) {
return urlQueryString(q, ENCODING);
}
/**
* Method which creates string with parameters and its value in format URL
*
* @param q map in which keys if name of parameters, value - value of parameters.
* @param enc encoding of this string
* @return string with parameters and its value in format URL
*/
public static String urlQueryString(Map<String, String> q, String enc) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> e : q.entrySet()) {
String name = encode(e.getKey(), enc);
sb.append(name);
if (e.getValue() != null) {
sb.append('=');
sb.append(encode(e.getValue(), enc));
}
sb.append('&');
}
if (sb.length() > 0)
sb.setLength(sb.length() - 1);
return sb.toString();
}
/**
* Translates a string into application/x-www-form-urlencoded format using a specific encoding scheme.
* This method uses the supplied encoding scheme to obtain the bytes for unsafe characters
*
* @param s string for encoding
* @param enc new encoding
* @return the translated String.
*/
private static String encode(String s, String enc) {
try {
return URLEncoder.encode(s, enc);
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Can't encode with supplied encoding: " + enc, e);
}
}
/**
* Decodes a application/x-www-form-urlencoded string using a specific encoding scheme. The supplied
* encoding is used to determine what characters are represented by any consecutive sequences of the
* form "%xy".
*
* @param s string for decoding
* @param enc the name of a supported character encoding
* @return the newly decoded String
*/
private static String decode(String s, String enc) throws ParseException {
try {
return URLDecoder.decode(s, enc);
} catch (RuntimeException e) {
throw new ParseException(e);
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Can't decode with supplied encoding: " + enc, e);
}
}
static boolean isNullOrEmpty(String s) {
return s == null || s.isEmpty();
}
static String nullToEmpty(String string) {
return string == null ? "" : string;
}
}