/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* 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 org.parosproxy.paros.core.scanner;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.log4j.Logger;
import org.parosproxy.paros.network.HttpMessage;
/**
* Specialized variant able to handles OData URIs for the resource ID part<p/>
* It's focused on OData v2
*
* Example of query having a single unnamed id:<br/>
* http://services.odata.org/OData/OData.svc/Category(1)/Products?$top=2&$orderby=name
* <p/>
* Example of query having a composite (named) id:<br/>
* http://services.odata.org/OData/OData.svc/DisplayItem(key1=2L,key2='B0EB1CA')
* <p/>
* Reference: <br/>
* http://www.odata.org/documentation/uri-conventions
*
*/
public class VariantODataIdQuery implements Variant {
private static final Logger log = Logger.getLogger(VariantODataIdQuery.class);
/**
* In order to identify the unnamed id we add this prefix to the resource
* name *
*/
public static final String RESOURCE_ID_PREFIX = "__ID__";
/**
* It's optional to have a resource parameter Set it to null of there is no
* such parameter in the URI
*/
private ResourceParameter resourceParameter = null;
// Extract the ID of a resource including the surrounding quote
// First group is the resource_name
// Second group is the ID (quote will be taken as part of the value)
private static final Pattern patternResourceIdentifierUnquoted = Pattern.compile("/(\\w*)\\(([\\w']*)\\)");
// Detect a section containing a composite IDs
private static final Pattern patternResourceMultipleIdentifier = Pattern.compile("/\\w*\\((.*)\\)");
// Extract the detail of the multiples IDs
private static final Pattern patternResourceMultipleIdentifierDetail = Pattern.compile("(\\w*)=([\\w']*)");
// Not very clean, should be improved.
// Save part of the URI before and after the section containing the composite IDs
private String beforeMultipleIDs = null;
private String afterMultipleIDs = null;
// Store the composite IDs if any
private List<NameValuePair> listParams = null;
@Override
public void setMessage(HttpMessage msg) {
URI uri = msg.getRequestHeader().getURI();
parse(uri);
}
private void parse(URI uri) {
try {
resourceParameter = null;
beforeMultipleIDs = null;
afterMultipleIDs = null;
listParams = null;
String path = uri.getPath();
if (path != null) {
// Detection of the resource and resource id (if any)
String resourceName = "";
String resourceID;
// check for single ID (unnamed)
Matcher matcher = patternResourceIdentifierUnquoted.matcher(path);
if (matcher.find()) {
resourceName = matcher.group(1);
resourceID = matcher.group(2);
String subString = resourceName + "(" + resourceID + ")";
int begin = path.indexOf(subString);
int end = begin + subString.length();
String beforeSubstring = path.substring(0, begin);
String afterSubstring = path.substring(end);
resourceParameter = new ResourceParameter(resourceName, resourceID, beforeSubstring, afterSubstring);
} else {
matcher = patternResourceMultipleIdentifier.matcher(path);
if (matcher.find()) {
// We've found a composite identifier. i.e: /Resource(field1=a,field2=3)
String multipleIdentifierSection = matcher.group(1);
int begin = path.indexOf(multipleIdentifierSection);
int end = begin + multipleIdentifierSection.length();
beforeMultipleIDs = path.substring(0, begin);
afterMultipleIDs = path.substring(end);
listParams = new ArrayList<>();
matcher = patternResourceMultipleIdentifierDetail.matcher(multipleIdentifierSection);
int i = 1;
while (matcher.find()) {
String paramName = matcher.group(1);
String value = matcher.group(2);
NameValuePair vp = new NameValuePair(NameValuePair.TYPE_QUERY_STRING, paramName, value, i++);
listParams.add(vp);
}
}
}
}
} catch (URIException e) {
log.error(e.getMessage() + uri, e);
}
}
@Override
public Vector<NameValuePair> getParamList() {
Vector<NameValuePair> params = new Vector<>();
if (resourceParameter != null) {
params.add(new NameValuePair(NameValuePair.TYPE_QUERY_STRING,
resourceParameter.getParameterName(), resourceParameter.getValue(), 1));
}
if (listParams != null) {
params.addAll(listParams);
}
return params;
}
@Override
public String setParameter(HttpMessage msg, NameValuePair originalPair, String param, String value) {
// TODO: Implement correctly escaped vs. non-escaped params
// Check if the parameter is a resource parameter
if (resourceParameter != null && resourceParameter.getParameterName().equals(param)) {
String query = value;
String modifiedPath = resourceParameter.getModifiedPath(value);
try {
msg.getRequestHeader().getURI().setPath(modifiedPath);
} catch (URIException e) {
throw new RuntimeException("Error with uri " + modifiedPath, e);
} catch (NullPointerException e) {
throw new RuntimeException("Error with uri " + modifiedPath, e);
}
return query;
} else if (listParams != null) {
// Check for composite ID
StringBuilder sb = new StringBuilder();
StringBuilder sbQuery = new StringBuilder();
sb.append(beforeMultipleIDs);
boolean firstPass = true;
for (NameValuePair nv : listParams) {
if (firstPass) {
firstPass = false;
} else {
sbQuery.append(",");
}
sbQuery.append(nv.getName()).append("=");
if (nv.getName().equals(param)) {
sbQuery.append(value);
} else {
sbQuery.append(nv.getValue());
}
}
sb.append(sbQuery);
sb.append(afterMultipleIDs);
String path = sb.toString();
String query = sbQuery.toString();
try {
msg.getRequestHeader().getURI().setPath(path);
} catch (URIException | NullPointerException e) {
throw new RuntimeException("Error with uri " + path, e);
}
return query;
}
return "";
}
@Override
public String setEscapedParameter(HttpMessage msg, NameValuePair originalPair, String param, String value) {
// TODO: Implement correctly escaped vs. non-escaped params
return setParameter(msg, originalPair, param, value);
}
/**
* Store the ID of a resource and related data
*/
static class ResourceParameter {
private String parameterName;
private String resourceName;
private String originalValue;
private String pathBeforeParameter;
private String pathAfterParamter;
/**
* @param parameterName
* @param originalValue
* @param pathBeforeParameter
* @param pathAfterParameter
*/
public ResourceParameter(String resourceName, String originalValue, String pathBeforeParameter, String pathAfterParameter) {
super();
this.resourceName = resourceName;
this.parameterName = RESOURCE_ID_PREFIX + resourceName;
this.originalValue = originalValue;
this.pathBeforeParameter = pathBeforeParameter;
this.pathAfterParamter = pathAfterParameter;
}
/**
* @return
*/
public String getValue() {
return this.originalValue;
}
public String getParameterName() {
return this.parameterName;
}
public String getModifiedPath(String newIdValue) {
StringBuilder builder = new StringBuilder();
builder.append(this.pathBeforeParameter)
.append(this.resourceName)
.append("(")
.append(newIdValue)
.append(")")
.append(this.pathAfterParamter);
return builder.toString();
}
}
}