package com.grendelscan.proxy; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.StringEntity; import org.apache.http.message.BasicHttpEntityEnclosingRequest; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpRequestHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.grendelscan.commons.MimeUtils; import com.grendelscan.commons.StringUtils; import com.grendelscan.commons.http.HttpCloner; import com.grendelscan.commons.http.HttpUtils; import com.grendelscan.commons.http.RequestOptions; import com.grendelscan.commons.http.transactions.StandardHttpTransaction; import com.grendelscan.commons.http.transactions.TransactionSource; import com.grendelscan.commons.http.transactions.UnrequestableTransaction; public abstract class AbstractProxyRequestHandler implements HttpRequestHandler { private static final Logger LOGGER = LoggerFactory .getLogger(AbstractProxyRequestHandler.class); protected AbstractProxy proxy; protected int remoteSSLPort; protected boolean ssl; private final RequestOptions proxyRequestOptions; static final Pattern typeChangePattern = Pattern.compile( "type\\s*=\\s*['\"]?hidden['\"]?+", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); // private StandardHttpTransaction getRequestToUse(HttpRequest request) // throws IOException // { // Destination destination = getDestination(request); // String host = destination.host; // int port = destination.port; // String uri = request.getRequestLine().getUri(); // if (ssl) // { // port = remoteSSLPort; // } // UnvalidatedHttpRequest requestToUse; // byte body[] = null; // if (request instanceof BasicHttpEntityEnclosingRequest) // { // HttpEntity entity = ((BasicHttpEntityEnclosingRequest) // request).getEntity(); // body = HttpUtils.entityToByteArray(entity, 0); // requestToUse = new // UnvalidatedHttpEntityRequest(request.getRequestLine().getMethod(), uri, // host, port, ssl, body); // } // else // { // requestToUse = new // UnvalidatedHttpRequest(request.getRequestLine().getMethod(), uri, host, // port, ssl); // } // requestToUse.addHeaders(request.getAllHeaders()); // fixRequestHeaders(requestToUse); // return requestToUse; // } static final Pattern hiddenFieldsPattern = Pattern.compile( "(<input[^>]*\\s*type\\s*=\\s*[\"']?hidden[\"']?[^>]*>)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); static final Pattern fieldNameQuotePattern = Pattern.compile( "name\\s*=\\s*(.)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); public AbstractProxyRequestHandler(final AbstractProxy proxy, final boolean ssl, final int remoteSSLPort) { this.proxy = proxy; this.ssl = ssl; this.remoteSSLPort = remoteSSLPort; proxyRequestOptions = new RequestOptions(); proxyRequestOptions.followRedirects = false; proxyRequestOptions.handleSessions = false; proxyRequestOptions.ignoreRestrictions = true; proxyRequestOptions.testTransaction = false; // Internal proxy logic // will submit to // categorizer as needed proxyRequestOptions.reason = "Internal proxy"; } protected StandardHttpTransaction checkRequestIntercept( final StandardHttpTransaction transaction) { if (Scan.getScanSettings().isInterceptRequests()) { if (StandardInterceptFilter.matchesFilters(Scan.getScanSettings() .getReadOnlyRequestInterceptFilters(), transaction)) { return InterceptionComposite.showRequestIntercept(transaction); } } return transaction; } /** * This doesn't need to return a transaction because the response intercept * will never create a new transaction * * @param transaction * @return */ protected boolean checkResponseIntercept( final StandardHttpTransaction transaction) { if (Scan.getScanSettings().isInterceptResponses()) { if (StandardInterceptFilter.matchesFilters(Scan.getScanSettings() .getReadOnlyResponseInterceptFilters(), transaction)) { return InterceptionComposite.showResponseIntercept(transaction); } } return false; } protected void errorMessage(final HttpResponse response, final int code, final String reason, final String body) { response.setStatusCode(code); response.setReasonPhrase(reason); StringEntity entity = null; try { entity = new StringEntity(body); } catch (UnsupportedEncodingException e) { LOGGER.error("Unsupported encoding: " + e.toString(), e); } response.setEntity(entity); response.addHeader("Server", "Grendel-Scan proxy"); response.addHeader("Content-Length", String.valueOf(body.length())); } protected void forbiddenMessage(final HttpRequest httpRequest, final HttpResponse response) { String body = "The scanner is not allowed to access the requested URL (" + httpRequest.getRequestLine().getUri() + ")."; errorMessage(response, 403, "Forbidden", body); } protected abstract Destination getDestination(HttpRequest request); protected String getNameAttributeQuoteCharacter(final String html) { Matcher m = fieldNameQuotePattern.matcher(html); if (m.find()) { String quote = m.group(1); if (quote.equals("'") || quote.equals("\"")) { return quote; } } return ""; } protected byte[] getProcessedResponseBody( final StandardHttpTransaction transaction) { byte[] body = transaction.getResponseWrapper().getBody(); if (body == null || body.length == 0) { return new byte[0]; } if (Scan.getScanSettings().isRevealHiddenFields() && MimeUtils.isHtmlMimeType(transaction.getResponseWrapper() .getHeaders().getMimeType())) { String responseBody = new String(transaction.getResponseWrapper() .getBody(), StringUtils.getDefaultCharset()); while (true) { Matcher hiddenFieldMatcher = hiddenFieldsPattern .matcher(responseBody); if (hiddenFieldMatcher.find()) // We found a hidden field { String field = hiddenFieldMatcher.group(1); String quote = getNameAttributeQuoteCharacter(field); Pattern fieldNamePattern; if (!quote.equals("")) { fieldNamePattern = Pattern.compile("name\\s*=\\s*" + quote + "([^" + quote + "]+)" + quote, Pattern.DOTALL | Pattern.CASE_INSENSITIVE); } else { fieldNamePattern = Pattern.compile( "name\\s*=\\s*(\\S+)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); } Matcher fieldNameMatcher = fieldNamePattern.matcher(field); String name = "Unknown name"; if (fieldNameMatcher.find()) { name = fieldNameMatcher.group(1); } String newField = typeChangePattern.matcher(field) .replaceFirst("type=\"text\""); responseBody = responseBody .replace( field, "\n<br/><span style=\"font-weight: bold; color: black;background-color: white\">" + "Hidden field \"" + name + "\": " + newField + "</span><br/>\n"); // responseBody = hiddenFieldMatcher.replaceFirst( // "<br><span style=\"font-weight: bold; color: black;background-color: white\">" // + // "Hidden field \"" + name + // "\"$1 type=\"text\" $2</span><br>"); } else { break; } } body = responseBody.getBytes(StringUtils.getDefaultCharset()); } else { body = transaction.getResponseWrapper().getBody(); } return body; } // protected void fixRequestHeaders(UnvalidatedHttpRequest request) // { // // Get rid of proxy and connection-related headers // request.removeHeaders("Proxy-Connection"); // request.removeHeaders("Connection"); // request.removeHeaders("Keep-Alive"); // // // An easy way to get rid of compression. Need a more elegant solution to // handle it later // request.removeHeaders("Accept-Encoding"); // // // request.removeHeaders("Proxy-Authorization"); // // } @Override public void handle(final HttpRequest request, final HttpResponse response, final HttpContext context) throws IOException { LOGGER.debug("Proxy received request for: " + request.getRequestLine().getUri()); StandardHttpTransaction transaction = new StandardHttpTransaction( TransactionSource.PROXY, -1); transaction.getRequestWrapper().getHeaders() .addHeaders(request.getAllHeaders()); transaction.getRequestWrapper().setSecure(ssl); transaction.getRequestWrapper().setMethod( request.getRequestLine().getMethod()); transaction.getRequestWrapper().setURI( request.getRequestLine().getUri(), true); transaction.getRequestWrapper().setVersion( request.getRequestLine().getProtocolVersion()); transaction.setRequestOptions(proxyRequestOptions); if (request instanceof BasicHttpEntityEnclosingRequest) { HttpEntity entity = ((BasicHttpEntityEnclosingRequest) request) .getEntity(); transaction.getRequestWrapper().setBody( HttpUtils.entityToByteArray(entity, 0)); } boolean testUninterceptedTransaction = Scan.getScanSettings() .isTestProxyRequests(); if (!Scan .getScanSettings() .getUrlFilters() .isUriAllowed( transaction.getRequestWrapper().getAbsoluteUriString())) { if (Scan.getScanSettings().isAllowAllProxyRequests()) { testUninterceptedTransaction = false; } else { forbiddenMessage(request, response); return; } } StandardHttpTransaction postInterceptTransaction = checkRequestIntercept(transaction); boolean intercepted = false; if (postInterceptTransaction != transaction) { transaction = postInterceptTransaction; intercepted = true; } try { transaction.execute(); } catch (UnrequestableTransaction e) { LOGGER.warn("Proxy request unrequestable (" + transaction.getRequestWrapper().getAbsoluteUriString() + "): " + e.toString(), e); } catch (InterruptedScanException e) { LOGGER.info("Scan aborted: " + e.toString(), e); return; } intercepted = checkResponseIntercept(transaction) || intercepted; if (testUninterceptedTransaction || intercepted && Scan.getScanSettings().isTestInterceptedRequests()) { Scan.getInstance().getCategorizerQueue() .addTransaction(transaction); } if (transaction.getResponseWrapper() == null) { errorMessage(response, 500, "Proxy error", "Unknown problem with Grendel-Scan proxy. The remote host may be unreachable"); } else { HttpEntity entity = new ByteArrayEntity( getProcessedResponseBody(transaction)); response.setEntity(entity); for (Header header : transaction.getResponseWrapper().getHeaders() .getReadOnlyHeaders()) { response.addHeader(HttpCloner.clone(header)); } response.setStatusLine(HttpCloner.clone(transaction .getResponseWrapper().getStatusLine())); } } }