package io.github.ibuildthecloud.gdapi.request.parser;
import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException;
import io.github.ibuildthecloud.gdapi.factory.SchemaFactory;
import io.github.ibuildthecloud.gdapi.model.Resource;
import io.github.ibuildthecloud.gdapi.request.ApiRequest;
import io.github.ibuildthecloud.gdapi.util.RequestUtils;
import io.github.ibuildthecloud.gdapi.util.ResponseCodes;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.lang3.StringUtils;
public class DefaultApiRequestParser implements ApiRequestParser {
public static final String DEFAULT_OVERRIDE_URL_HEADER = "X-API-request-url";
public static final String DEFAULT_OVERRIDE_CLIENT_IP_HEADER = "X-API-client-ip";
public static final String FORWARDED_FOR_HEADER = "X-Forwarded-For";
public static final String FORWARDED_HOST_HEADER = "X-Forwarded-Host";
public static final String FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";
public static final String FORWARDED_PORT_HEADER = "X-Forwarded-Port";
public static final String HOST_HEADER = "Host";
public static final String HTML = "html";
public static final String JSON = "json";
ServletFileUpload servletFileUpload;
int maxUploadSize = 100 * 1024;
boolean allowClientOverrideHeaders = false;
String overrideUrlHeader = DEFAULT_OVERRIDE_URL_HEADER;
String overrideClientIpHeader = DEFAULT_OVERRIDE_CLIENT_IP_HEADER;
Set<String> allowedFormats;
String trimPrefix;
@Override
public boolean parse(ApiRequest apiRequest) throws IOException {
HttpServletRequest request = apiRequest.getServletContext().getRequest();
apiRequest.setLocale(getLocale(apiRequest, request));
apiRequest.setMethod(parseMethod(apiRequest, request));
apiRequest.setAction(parseAction(apiRequest, request));
apiRequest.setRequestParams(parseParams(apiRequest, request));
apiRequest.setRequestUrl(parseRequestUrl(apiRequest, request));
apiRequest.setClientIp(parseClientIp(apiRequest, request));
apiRequest.setResponseUrlBase(parseResponseUrlBase(apiRequest, request));
apiRequest.setVersion(parseVersion(apiRequest, request));
apiRequest.setResponseFormat(parseResponseType(apiRequest, request));
apiRequest.setQueryString(parseQueryString(apiRequest, request));
parsePath(apiRequest, request);
return true;
}
protected Locale getLocale(ApiRequest apiRequest, HttpServletRequest request) {
return request.getLocale();
}
protected String parseQueryString(ApiRequest apiRequest, HttpServletRequest request) {
return request.getQueryString();
}
protected String parseMethod(ApiRequest apiRequest, HttpServletRequest request) {
String method = request.getParameter("_method");
if (method == null)
method = request.getMethod();
return method;
}
protected String parseAction(ApiRequest apiRequest, HttpServletRequest request) {
if ("POST".equals(apiRequest.getMethod())) {
return request.getParameter(Resource.ACTION);
}
return null;
}
@SuppressWarnings("unchecked")
protected Map<String, Object> parseParams(ApiRequest apiRequest, HttpServletRequest request) throws IOException {
try {
Map<String, Object> multiPart = parseMultipart(request);
return multiPart == null ? request.getParameterMap() : multiPart;
} catch (IOException e) {
if (e.getCause() instanceof FileUploadBase.SizeLimitExceededException)
throw new ClientVisibleException(ResponseCodes.REQUEST_ENTITY_TOO_LARGE);
throw e;
}
}
protected Map<String, Object> parseMultipart(HttpServletRequest request) throws IOException {
if (!ServletFileUpload.isMultipartContent(request))
return null;
Map<String, List<String>> params = new HashMap<String, List<String>>();
try {
List<FileItem> items = servletFileUpload.parseRequest(request);
for (FileItem item : items) {
if (item.isFormField()) {
List<String> values = params.get(item.getFieldName());
if (values == null) {
values = new ArrayList<String>();
params.put(item.getFieldName(), values);
}
values.add(item.getString());
}
}
Map<String, Object> result = new HashMap<String, Object>();
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
List<String> values = entry.getValue();
result.put(entry.getKey(), values.toArray(new String[values.size()]));
}
return result;
} catch (FileUploadException e) {
throw new IOException(e);
}
}
protected String getOverrideHeader(HttpServletRequest request, String header, String defaultValue) {
return getOverrideHeader(request, header, defaultValue, true);
}
protected String getOverrideHeader(HttpServletRequest request, String header, String defaultValue, boolean checkSetting) {
if (checkSetting && !isAllowClientOverrideHeaders()) {
return defaultValue;
}
// Need to handle comma separated hosts in X-Forwarded-For
String value = request.getHeader(header);
if (value != null) {
String[] ips = StringUtils.split(value, ",");
if (ips.length > 0) {
return StringUtils.trim(ips[0]);
}
}
return defaultValue;
}
protected String parseClientIp(ApiRequest apiRequest, HttpServletRequest request) {
String clientIp = request.getRemoteAddr();
clientIp = getOverrideHeader(request, overrideClientIpHeader, clientIp);
clientIp = getOverrideHeader(request, FORWARDED_FOR_HEADER, clientIp, false);
if (StringUtils.isNotBlank(clientIp)) {
// This is to deal with situations in which the x-forwarded-for is incorrect and has the remote port in it
String[] parts = clientIp.split("[:]");
if (parts.length == 2) {
return parts[0];
}
}
return clientIp;
}
/**
* Constructs the request URL based off of standard headers in the request, falling back to the HttpServletRequest.getRequestURL()
* if the headers aren't available. Here is the ordered list of how we'll attempt to construct the URL:
* - x-api-request-url
* - x-forwarded-proto://x-forwarded-host:x-forwarded-port/HttpServletRequest.getRequestURI()
* - x-forwarded-proto://x-forwarded-host/HttpServletRequest.getRequestURI()
* - x-forwarded-proto://host:x-forwarded-port/HttpServletRequest.getRequestURI()
* - x-forwarded-proto://host/HttpServletRequest.getRequestURI() request.getRequestURL()
*
* Additional notes:
* - With x-api-request-url, the query string is passed, it will be dropped to match the other formats.
* - If the x-forwarded-host/host header has a port and x-forwarded-port has been passed, x-forwarded-port will be used.
*/
protected String parseRequestUrl(ApiRequest apiRequest, HttpServletRequest request) {
// Get url from custom x-api-request-url header
String requestUrl = getOverrideHeader(request, overrideUrlHeader, null);
if (requestUrl != null) {
String[] parts = requestUrl.split("\\?", 2);
return parts[0];
}
// Get url from standard headers
requestUrl = getUrlFromStandardHeaders(request);
if (requestUrl != null) {
return requestUrl;
}
// Use incoming url
return request.getRequestURL().toString();
}
private String getUrlFromStandardHeaders(HttpServletRequest request) {
String host = getOverrideHeader(request, FORWARDED_HOST_HEADER, null, false);
if (host == null) {
host = getOverrideHeader(request, HOST_HEADER, null, false);
}
String port = getOverrideHeader(request, FORWARDED_PORT_HEADER, null, false);
String xForwardedProto = getOverrideHeader(request, FORWARDED_PROTO_HEADER, null, false);
if (xForwardedProto == null && isHttpsPort(host, port)) {
xForwardedProto = "https";
}
if (xForwardedProto == null || host == null) {
return null;
}
if (StringUtils.equals(port, "443") || StringUtils.equals(port, "80")) {
port = null; // Don't include default ports in url
}
if (port != null && host.contains(":")) {
// Have to strip the port that is in the host. Handle IPv6, which has this format: [::1]:8080
if ((host.startsWith("[") && host.contains("]:")) || !host.startsWith("[")) {
host = host.substring(0, host.lastIndexOf(":"));
}
}
StringBuilder builder = new StringBuilder(xForwardedProto).append("://").append(host);
if (port != null) {
builder.append(":").append(port);
}
builder.append(request.getRequestURI());
return builder.toString();
}
protected String parseResponseUrlBase(ApiRequest apiRequest, HttpServletRequest request) {
String servletPath = request.getServletPath();
String requestUrl = apiRequest.getRequestUrl();
int index = requestUrl.lastIndexOf(servletPath);
if (index == -1) {
try {
/*
* Fallback, if we can't find servletPath in requestUrl, then we just assume the base is the root of the web request
*/
URL url = new URL(requestUrl);
StringBuilder buffer = new StringBuilder(url.getProtocol()).append("://").append(url.getHost());
if (url.getPort() != -1) {
buffer.append(":").append(url.getPort());
}
return buffer.toString();
} catch (MalformedURLException e) {
throw new ClientVisibleException(ResponseCodes.NOT_FOUND);
}
} else {
return requestUrl.substring(0, index);
}
}
protected String parseVersion(ApiRequest apiRequest, HttpServletRequest request) {
return parseVersion(request.getServletPath());
}
@Override
public String parseVersion(String servletPath) {
servletPath = trimPrefix(servletPath.replaceAll("//+", "/"));
if (!servletPath.startsWith("/") || servletPath.length() < 2)
return null;
return servletPath.split("/")[1];
}
protected String trimPrefix(String path) {
if (trimPrefix != null && path.startsWith(trimPrefix)) {
return path.substring(trimPrefix.length());
}
return path;
}
protected String parseResponseType(ApiRequest apiRequest, HttpServletRequest request) {
String format = request.getParameter("_format");
if (format != null) {
format = format.toLowerCase().trim();
}
/* Format specified */
if (format != null && allowedFormats.contains(format)) {
return format;
}
// User agent has Mozilla and browser accepts */*
if (RequestUtils.isBrowser(request, true)) {
return HTML;
} else {
return JSON;
}
}
protected void parsePath(ApiRequest apiRequest, HttpServletRequest request) {
if (apiRequest.getVersion() == null)
return;
String servletPath = request.getServletPath();
servletPath = trimPrefix(servletPath.replaceAll("//+", "/"));
String versionPrefix = "/" + apiRequest.getVersion();
if (!servletPath.startsWith(versionPrefix)) {
return;
}
String[] parts = servletPath.substring(versionPrefix.length()).split("/");
String typeName = indexValue(parts, 1);
String id = indexValue(parts, 2);
String link = indexValue(parts, 3);
if (StringUtils.isBlank(typeName)) {
return;
} else {
SchemaFactory schemaFactory = apiRequest.getSchemaFactory();
if (schemaFactory == null) {
apiRequest.setType(typeName);
} else {
String singleType = apiRequest.getSchemaFactory().getSingularName(typeName);
apiRequest.setType(singleType == null ? typeName : singleType);
}
}
if (StringUtils.isBlank(id)) {
return;
} else {
apiRequest.setId(id);
}
if (StringUtils.isBlank(link)) {
return;
} else {
apiRequest.setLink(link);
}
}
protected String indexValue(String[] array, int index) {
if (array.length <= index) {
return null;
}
String value = array[index];
return value == null ? value : value.trim();
}
@PostConstruct
public void init() {
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(maxUploadSize * 2);
servletFileUpload = new ServletFileUpload(factory);
servletFileUpload.setFileSizeMax(maxUploadSize);
servletFileUpload.setSizeMax(maxUploadSize);
if (allowedFormats == null) {
allowedFormats = new HashSet<String>();
allowedFormats.add(HTML);
allowedFormats.add(JSON);
}
}
public boolean isHttpsPort(String host, String port) {
return false;
}
public Set<String> getAllowedFormats() {
return allowedFormats;
}
public void setAllowedFormats(Set<String> allowedFormats) {
this.allowedFormats = allowedFormats;
}
public boolean isAllowClientOverrideHeaders() {
return allowClientOverrideHeaders;
}
public void setAllowClientOverrideHeaders(boolean allowClientOverrideHeaders) {
this.allowClientOverrideHeaders = allowClientOverrideHeaders;
}
public String getTrimPrefix() {
return trimPrefix;
}
public void setTrimPrefix(String trimPrefix) {
this.trimPrefix = trimPrefix;
}
}