diff --git a/commons/pom.xml b/commons/pom.xml
index 2b502b0c..75c31503 100644
--- a/commons/pom.xml
+++ b/commons/pom.xml
@@ -27,6 +27,10 @@
org.apache.httpcomponentshttpclient
+
+ org.apache.httpcomponents
+ httpmime
+ com.sleepycatje
diff --git a/modules/src/main/java/org/archive/modules/CoreAttributeConstants.java b/modules/src/main/java/org/archive/modules/CoreAttributeConstants.java
index f0579b61..f58c56e1 100644
--- a/modules/src/main/java/org/archive/modules/CoreAttributeConstants.java
+++ b/modules/src/main/java/org/archive/modules/CoreAttributeConstants.java
@@ -118,10 +118,11 @@ public interface CoreAttributeConstants {
public static final String A_HTTP_RESPONSE_HEADERS = "http-response-headers";
public static final String A_HTTP_AUTH_CHALLENGES = "http-auth-challenges";
-
- // FORMS support - a persistent member (survives frontier enqueue/dequeue/retries)
+
+ // FORMS support - persistent members (survive frontier enqueue/dequeue/retries)
public static final String A_SUBMIT_DATA = "submit-data";
-
+ public static final String A_SUBMIT_ENCTYPE = "submit-enctype";
+
// arbitrary additions to WARC response record headers
public static final String A_WARC_RESPONSE_HEADERS = "warc-response-headers";
diff --git a/modules/src/main/java/org/archive/modules/CrawlURI.java b/modules/src/main/java/org/archive/modules/CrawlURI.java
index dc666061..6fbab82d 100644
--- a/modules/src/main/java/org/archive/modules/CrawlURI.java
+++ b/modules/src/main/java/org/archive/modules/CrawlURI.java
@@ -32,6 +32,7 @@ import static org.archive.modules.CoreAttributeConstants.A_NONFATAL_ERRORS;
import static org.archive.modules.CoreAttributeConstants.A_PREREQUISITE_URI;
import static org.archive.modules.CoreAttributeConstants.A_SOURCE_TAG;
import static org.archive.modules.CoreAttributeConstants.A_SUBMIT_DATA;
+import static org.archive.modules.CoreAttributeConstants.A_SUBMIT_ENCTYPE;
import static org.archive.modules.CoreAttributeConstants.A_WARC_RESPONSE_HEADERS;
import static org.archive.modules.SchedulingConstants.NORMAL;
import static org.archive.modules.fetcher.FetchStatusCodes.S_BLOCKED_BY_CUSTOM_PROCESSOR;
@@ -256,7 +257,7 @@ implements Reporter, Serializable, OverlayContext, Comparable {
*/
private static final Collection persistentKeys
= new CopyOnWriteArrayList(
- new String [] {A_CREDENTIALS_KEY, A_HTTP_AUTH_CHALLENGES, A_SUBMIT_DATA, A_WARC_RESPONSE_HEADERS, A_ANNOTATIONS});
+ new String [] {A_CREDENTIALS_KEY, A_HTTP_AUTH_CHALLENGES, A_SUBMIT_DATA, A_WARC_RESPONSE_HEADERS, A_ANNOTATIONS, A_SUBMIT_ENCTYPE});
/** maximum length for pathFromSeed/hopsPath; longer truncated with leading counter **/
private static final int MAX_HOPS_DISPLAYED = 50;
diff --git a/modules/src/main/java/org/archive/modules/fetcher/FetchHTTPRequest.java b/modules/src/main/java/org/archive/modules/fetcher/FetchHTTPRequest.java
index 19a47771..a9038e18 100644
--- a/modules/src/main/java/org/archive/modules/fetcher/FetchHTTPRequest.java
+++ b/modules/src/main/java/org/archive/modules/fetcher/FetchHTTPRequest.java
@@ -25,6 +25,7 @@ import static org.archive.modules.recrawl.RecrawlAttributeConstants.A_STATUS;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
@@ -34,6 +35,7 @@ import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -45,6 +47,7 @@ import javax.net.ssl.SSLSocket;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.lang.StringUtils;
+import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders;
@@ -65,7 +68,6 @@ import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.AbstractExecutionAwareRequest;
import org.apache.http.client.protocol.HttpClientContext;
-import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.MessageConstraints;
import org.apache.http.config.Registry;
@@ -82,7 +84,8 @@ import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentLengthStrategy;
import org.apache.http.entity.ContentType;
-import org.apache.http.entity.StringEntity;
+import org.apache.http.entity.mime.HttpMultipartMode;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.DefaultBHttpClientConnection;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
@@ -105,6 +108,7 @@ import org.archive.modules.credential.Credential;
import org.archive.modules.credential.HtmlFormCredential;
import org.archive.modules.credential.HttpAuthenticationCredential;
import org.archive.modules.extractor.LinkContext;
+import org.archive.modules.forms.HTMLForm.NameValue;
import org.archive.modules.net.CrawlHost;
import org.archive.modules.net.CrawlServer;
import org.archive.modules.net.ServerCache;
@@ -113,7 +117,7 @@ import org.archive.util.Recorder;
/**
* @contributor nlevitt
*/
-class FetchHTTPRequest {
+public class FetchHTTPRequest {
/**
* Implementation of {@link DnsResolver} that uses the server cache which is
@@ -183,12 +187,9 @@ class FetchHTTPRequest {
BasicExecutionAwareEntityEnclosingRequest postRequest = new BasicExecutionAwareEntityEnclosingRequest(
"POST", requestLineUri, httpVersion);
this.request = postRequest;
- String submitData = (String) curi.getData().get(CoreAttributeConstants.A_SUBMIT_DATA);
- if (submitData != null) {
- // XXX brittle, doesn't support multipart form data etc
- ContentType contentType = ContentType.create(URLEncodedUtils.CONTENT_TYPE, "UTF-8");
- StringEntity formEntity = new StringEntity(submitData, contentType);
- postRequest.setEntity(formEntity);
+ if (curi.containsDataKey(CoreAttributeConstants.A_SUBMIT_DATA)) {
+ HttpEntity entity = buildPostRequestEntity(curi);
+ postRequest.setEntity(entity);
}
} else {
this.request = new BasicExecutionAwareRequest("GET",
@@ -210,6 +211,79 @@ class FetchHTTPRequest {
populateHttpProxyCredential();
}
+ /**
+ * Returns a copy of the string with non-ascii characters replaced by their
+ * html numeric character reference in decimal (e.g. 〹).
+ *
+ *
+ * The purpose of this is to produce a multipart/formdata submission that
+ * any server should be able to handle, based on experiments using a modern
+ * browser (chromium 47.0.2526.106 for mac). What chromium posts depends on
+ * what it considers the character encoding of the page containing the form,
+ * and maybe other factors. It would be too complicated to try to simulate
+ * that behavior in heritrix.
+ *
+ *
+ * Instead what we do is approximately what the browser does when the form
+ * page is plain ascii. It html-escapes characters outside of the
+ * latin1/cp1252 range. Characters in the U+0080-U+00FF range are encoded in
+ * latin1/cp1252. That is the one way that we differ from chromium. We
+ * html-escape those characters (U+0080-U+00FF) as well. That way the http
+ * post is plain ascii, and should work regardless of which encoding the
+ * server expects.
+ *
+ *
+ * N.b. chromium doesn't indicate the encoding of the request in any way (no
+ * charset in the content-type or anything like that). Also of note is that
+ * when it considers the form page to be utf-8, it submits in utf-8. That's
+ * part of the complicated behavior we don't want to try to simulate.
+ */
+ public static String escapeForMultipart(String str) {
+ StringBuilder buf = new StringBuilder();
+ for (int i = 0; i < str.length(); ) {
+ int codepoint = str.codePointAt(i);
+ if (codepoint <= 0x7f) {
+ buf.appendCodePoint(codepoint);
+ } else {
+ buf.append("" + codepoint + ";");
+ }
+ i += Character.charCount(codepoint);
+ }
+ return buf.toString();
+ }
+
+ protected HttpEntity buildPostRequestEntity(CrawlURI curi) {
+ String enctype = (String) curi.getData().get(CoreAttributeConstants.A_SUBMIT_ENCTYPE);
+ if (enctype == null) {
+ enctype = ContentType.APPLICATION_FORM_URLENCODED.getMimeType();
+ }
+
+ @SuppressWarnings("unchecked")
+ List submitData = (List) curi.getData().get(CoreAttributeConstants.A_SUBMIT_DATA);
+
+ if (enctype.equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
+ LinkedList nvps = new LinkedList();
+ for (NameValue nv: submitData) {
+ nvps.add(new BasicNameValuePair(nv.name, nv.value));
+ }
+ try {
+ return new UrlEncodedFormEntity(nvps, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalStateException(e);
+ }
+ } else if (enctype.equals(ContentType.MULTIPART_FORM_DATA.getMimeType())) {
+ MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
+ entityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
+ for (NameValue nv: submitData) {
+ entityBuilder.addTextBody(escapeForMultipart(nv.name),
+ escapeForMultipart(nv.value));
+ }
+ return entityBuilder.build();
+ } else {
+ throw new IllegalStateException("unsupported form submission enctype='" + enctype + "'");
+ }
+ }
+
protected void configureRequestHeaders() {
if (fetcher.getAcceptCompression()) {
request.addHeader("Accept-Encoding", "gzip,deflate");
diff --git a/modules/src/main/java/org/archive/modules/forms/ExtractorHTMLForms.java b/modules/src/main/java/org/archive/modules/forms/ExtractorHTMLForms.java
index 231a1e05..0c672102 100644
--- a/modules/src/main/java/org/archive/modules/forms/ExtractorHTMLForms.java
+++ b/modules/src/main/java/org/archive/modules/forms/ExtractorHTMLForms.java
@@ -145,9 +145,11 @@ public class ExtractorHTMLForms extends Extractor {
CharSequence relevantSequence = cs.subSequence(offsetInt, cs.length());
String method = findAttributeValueGroup("(?i)^[^>]*\\smethod\\s*=\\s*([^>\\s]+)[^>]*>",1,relevantSequence);
String action = findAttributeValueGroup("(?i)^[^>]*\\saction\\s*=\\s*([^>\\s]+)[^>]*>",1,relevantSequence);
+ String enctype = findAttributeValueGroup("(?i)^[^>]*\\senctype\\s*=\\s*([^>\\s]+)[^>]*>",1,relevantSequence);
HTMLForm form = new HTMLForm();
form.setMethod(method);
- form.setAction(action);
+ form.setAction(action);
+ form.setEnctype(enctype);
for(CharSequence input : findGroups("(?i)(]*>)|(?form>)",1,relevantSequence)) {
String type = findAttributeValueGroup("(?i)^[^>]*\\stype\\s*=\\s*([^>\\s]+)[^>]*>",1,input);
String name = findAttributeValueGroup("(?i)^[^>]*\\sname\\s*=\\s*([^>\\s]+)[^>]*>",1,input);
diff --git a/modules/src/main/java/org/archive/modules/forms/FormLoginProcessor.java b/modules/src/main/java/org/archive/modules/forms/FormLoginProcessor.java
index 9a2d540d..b6ee86a2 100644
--- a/modules/src/main/java/org/archive/modules/forms/FormLoginProcessor.java
+++ b/modules/src/main/java/org/archive/modules/forms/FormLoginProcessor.java
@@ -266,10 +266,12 @@ public class FormLoginProcessor extends Processor implements Checkpointable {
CrawlURI submitCuri = curi.createCrawlURI(submitUrl, lc, Hop.SUBMIT);
submitCuri.setFetchType(FetchType.HTTP_POST);
submitCuri.getData().put(
- CoreAttributeConstants.A_SUBMIT_DATA,
- templateForm.asFormDataString(
+ CoreAttributeConstants.A_SUBMIT_DATA,
+ templateForm.formData(
getLoginUsername(),
getLoginPassword()));
+ submitCuri.getData().put(CoreAttributeConstants.A_SUBMIT_ENCTYPE,
+ templateForm.getEnctype());
submitCuri.setSchedulingDirective(SchedulingConstants.HIGH);
submitCuri.setForceFetch(true);
curi.getOutLinks().add(submitCuri);
diff --git a/modules/src/main/java/org/archive/modules/forms/HTMLForm.java b/modules/src/main/java/org/archive/modules/forms/HTMLForm.java
index 0634176b..198f5285 100644
--- a/modules/src/main/java/org/archive/modules/forms/HTMLForm.java
+++ b/modules/src/main/java/org/archive/modules/forms/HTMLForm.java
@@ -23,9 +23,7 @@ import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
-import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.lang.StringUtils;
-import org.archive.util.TextUtils;
/**
* Simple representation of a discovered HTML Form.
@@ -47,13 +45,14 @@ public class HTMLForm {
return str;
}
}
-
- String method;
- String action;
-
- List allInputs = new ArrayList();
- List candidateUsernameInputs = new ArrayList();
- List candidatePasswordInputs = new ArrayList();
+
+ protected String method;
+ protected String action;
+ protected String enctype;
+
+ protected List allInputs = new ArrayList();
+ protected List candidateUsernameInputs = new ArrayList();
+ protected List candidatePasswordInputs = new ArrayList();
/**
* Add a discovered INPUT, tracking it as potential
@@ -99,11 +98,19 @@ public class HTMLForm {
public String getAction() {
return action;
}
-
+
public void setAction(String action) {
this.action = action;
}
+ public String getEnctype() {
+ return enctype;
+ }
+
+ public void setEnctype(String enctype) {
+ this.enctype = enctype;
+ }
+
/**
* For now, we consider a POST form with only 1 password
* field and 1 potential username field (type text or email)
@@ -117,48 +124,29 @@ public class HTMLForm {
&& candidatePasswordInputs.size() == 1;
}
- /**
- * Create the NameValuePair array expected by HttpClient, merging
- * username and password into the appropriate value slots.
- *
- * @param username
- * @param password
- * @return
- * @deprecated specific to a particular FetchHTTP implementation based on commons-httpclient, use {@link #asFormDataString(String, String)}
- */
- public NameValuePair[] asHttpClientDataWith(String username, String password) {
- ArrayList data = new ArrayList(allInputs.size());
-
- for (FormInput input : allInputs) {
- if(input == candidateUsernameInputs.get(0)) {
- data.add(new NameValuePair(input.name,username));
- } else if(input == candidatePasswordInputs.get(0)) {
- data.add(new NameValuePair(input.name,password));
- } else if (StringUtils.isNotEmpty(input.name) && StringUtils.isNotEmpty(input.value)) {
- data.add(new NameValuePair(input.name,input.value));
- }
+ public static class NameValue {
+ public String name, value;
+ public NameValue(String name, String value) {
+ this.name = name;
+ this.value = value;
}
- return data.toArray(new NameValuePair[data.size()]);
}
-
- public String asFormDataString(String username, String password) {
- List nameVals = new LinkedList();
+ public LinkedList formData(String username, String password) {
+ LinkedList nameVals = new LinkedList();
for (FormInput input : allInputs) {
- if(input == candidateUsernameInputs.get(0)) {
- nameVals.add(TextUtils.urlEscape(input.name) + "=" + TextUtils.urlEscape(username));
+ if (input == candidateUsernameInputs.get(0)) {
+ nameVals.add(new NameValue(input.name, username));
} else if(input == candidatePasswordInputs.get(0)) {
- nameVals.add(TextUtils.urlEscape(input.name) + "=" + TextUtils.urlEscape(password));
+ nameVals.add(new NameValue(input.name, password));
} else if (StringUtils.isNotEmpty(input.name)
&& StringUtils.isNotEmpty(input.value)
&& (!"radio".equalsIgnoreCase(input.type)
&& !"checkbox".equals(input.type) || input.checked)) {
- nameVals.add(TextUtils.urlEscape(input.name) + "="
- + TextUtils.urlEscape(input.value));
+ nameVals.add(new NameValue(input.name, input.value));
}
}
-
- return StringUtils.join(nameVals, '&');
+ return nameVals;
}
public String toString() {
diff --git a/modules/src/test/java/org/archive/modules/fetcher/FetchHTTPTests.java b/modules/src/test/java/org/archive/modules/fetcher/FetchHTTPTests.java
index 91449e6f..174cb89b 100644
--- a/modules/src/test/java/org/archive/modules/fetcher/FetchHTTPTests.java
+++ b/modules/src/test/java/org/archive/modules/fetcher/FetchHTTPTests.java
@@ -53,10 +53,7 @@ import javax.net.ssl.SSLException;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.io.IOUtils;
-import org.apache.http.NameValuePair;
import org.apache.http.NoHttpResponseException;
-import org.apache.http.client.utils.URLEncodedUtils;
-import org.apache.http.message.BasicNameValuePair;
import org.archive.httpclient.ConfigurableX509TrustManager.TrustLevel;
import org.archive.modules.CoreAttributeConstants;
import org.archive.modules.CrawlMetadata;
@@ -65,6 +62,7 @@ import org.archive.modules.CrawlURI.FetchType;
import org.archive.modules.ProcessorTestBase;
import org.archive.modules.credential.HttpAuthenticationCredential;
import org.archive.modules.deciderules.RejectDecideRule;
+import org.archive.modules.forms.HTMLForm.NameValue;
import org.archive.modules.recrawl.FetchHistoryProcessor;
import org.archive.modules.revisit.ServerNotModifiedRevisit;
import org.archive.net.UURI;
@@ -856,16 +854,14 @@ public class FetchHTTPTests extends ProcessorTestBase {
CrawlURI curi = makeCrawlURI("http://localhost:7777/");
curi.setFetchType(FetchType.HTTP_POST);
- List params = new LinkedList();
- params.add(new BasicNameValuePair("name1", "value1"));
- params.add(new BasicNameValuePair("name1", "value2"));
- params.add(new BasicNameValuePair("funky name 2", "whoa crazy\t && 🍺 🍻 \n crazier \rooo"));
- String submitData = URLEncodedUtils.format(params, "UTF-8");
- assertEquals("name1=value1&name1=value2&funky+name+2=whoa+crazy%09+%26%26+%F0%9F%8D%BA+%F0%9F%8D%BB+%0A+crazier+%0Dooo", submitData);
+ List params = new LinkedList();
+ params.add(new NameValue("name1", "value1"));
+ params.add(new NameValue("name1", "value2"));
+ params.add(new NameValue("funky name 2", "whoa crazy\t && 🍺 🍻 \n crazier \rooo"));
+ curi.getData().put(CoreAttributeConstants.A_SUBMIT_DATA, params);
- curi.getData().put(CoreAttributeConstants.A_SUBMIT_DATA, submitData);
fetcher().process(curi);
-
+
assertTrue(httpRequestString(curi).startsWith("POST / HTTP/1.0\r\n"));
assertTrue(httpRequestString(curi).endsWith("\r\n\r\nname1=value1&name1=value2&funky+name+2=whoa+crazy%09+%26%26+%F0%9F%8D%BA+%F0%9F%8D%BB+%0A+crazier+%0Dooo"));
assertEquals(FetchType.HTTP_POST, curi.getFetchType());
diff --git a/modules/src/test/java/org/archive/modules/forms/FormLoginProcessorTest.java b/modules/src/test/java/org/archive/modules/forms/FormLoginProcessorTest.java
index 0fc297b5..84c9cd52 100644
--- a/modules/src/test/java/org/archive/modules/forms/FormLoginProcessorTest.java
+++ b/modules/src/test/java/org/archive/modules/forms/FormLoginProcessorTest.java
@@ -1,8 +1,8 @@
/*
* This file is part of the Heritrix web crawler (crawler.archive.org).
*
- * Licensed to the Internet Archive (IA) by one or more individual
- * contributors.
+ * Licensed to the Internet Archive (IA) by one or more individual
+ * contributors.
*
* The IA licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
@@ -21,13 +21,32 @@ package org.archive.modules.forms;
import static org.archive.modules.CoreAttributeConstants.A_WARC_RESPONSE_HEADERS;
-import org.archive.modules.CoreAttributeConstants;
+import java.io.ByteArrayOutputStream;
+
+import org.apache.commons.httpclient.URIException;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.http.HttpRequest;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.util.EntityUtils;
import org.archive.modules.CrawlURI;
import org.archive.modules.CrawlURI.FetchType;
import org.archive.modules.ProcessorTestBase;
+import org.archive.modules.fetcher.BasicExecutionAwareEntityEnclosingRequest;
+import org.archive.modules.fetcher.FetchHTTP;
+import org.archive.modules.fetcher.FetchHTTPRequest;
+import org.archive.modules.fetcher.FetchHTTPTests;
public class FormLoginProcessorTest extends ProcessorTestBase {
+ static class FetchHTTPRequestSpy extends FetchHTTPRequest {
+ public FetchHTTPRequestSpy(FetchHTTP fetcher, CrawlURI curi) throws URIException {
+ super(fetcher, curi);
+ }
+ public HttpRequest getRequest() {
+ return request;
+ }
+ }
+
public void testNoFormLogin() throws Exception {
CrawlURI curi = makeCrawlURI("http://example.com/");
@@ -38,7 +57,7 @@ public class FormLoginProcessorTest extends ProcessorTestBase {
p.process(curi);
- assertEquals(1, curi.getDataList(A_WARC_RESPONSE_HEADERS).size());
+ assertEquals(1, curi.getDataList(A_WARC_RESPONSE_HEADERS).size());
assertEquals("WARC-Simple-Form-Province-Status: 0,0,http://(com,example,)", curi.getDataList(A_WARC_RESPONSE_HEADERS).get(0));
}
@@ -48,6 +67,7 @@ public class FormLoginProcessorTest extends ProcessorTestBase {
HTMLForm form = new HTMLForm();
form.addField("text", "username-form-field", "");
form.addField("password", "password-form-field", "");
+ form.addField("hidden", "crazy🐒monkey", "úhóh");
form.setMethod("post");
form.setAction("/login");
curi.getDataList(ExtractorHTMLForms.A_HTML_FORM_OBJECTS).add(form);
@@ -57,6 +77,44 @@ public class FormLoginProcessorTest extends ProcessorTestBase {
p.setLoginPassword("********");
p.setApplicableSurtPrefix("http://(com,example,)");
+ p.process(curi);
+ assertEquals(1, curi.getDataList(A_WARC_RESPONSE_HEADERS).size());
+ assertEquals("WARC-Simple-Form-Province-Status: 0,0,http://(com,example,)", curi.getDataList(A_WARC_RESPONSE_HEADERS).get(0));
+ assertTrue(curi.getAnnotations().contains("submit:/login"));
+
+ assertEquals(1, curi.getOutLinks().size());
+ CrawlURI submitCuri = curi.getOutLinks().toArray(new CrawlURI[0])[0];
+ assertEquals("http://example.com/login", submitCuri.toString());
+ assertEquals(FetchType.HTTP_POST, submitCuri.getFetchType());
+
+ FetchHTTPRequestSpy reqSpy = new FetchHTTPRequestSpy(
+ FetchHTTPTests.newTestFetchHttp(getClass().getName()),
+ submitCuri);
+ assertTrue(reqSpy.getRequest() instanceof BasicExecutionAwareEntityEnclosingRequest);
+ BasicExecutionAwareEntityEnclosingRequest req = (BasicExecutionAwareEntityEnclosingRequest) reqSpy.getRequest();
+ assertTrue(req.getEntity() instanceof UrlEncodedFormEntity);
+ assertTrue(req.toString().startsWith("POST /login"));
+ assertEquals("username-form-field=jdoe&password-form-field=********&crazy%F0%9F%90%92monkey=%C3%BAh%C3%B3h",
+ EntityUtils.toString(req.getEntity()));
+ }
+
+ public void testMultipartFormLogin() throws Exception {
+ CrawlURI curi = makeCrawlURI("http://example.com/");
+
+ HTMLForm form = new HTMLForm();
+ form.addField("text", "username-form-field", "");
+ form.addField("password", "password-form-field", "");
+ form.addField("hidden", "crazy🐒monkey", "úhóh");
+ form.setMethod("post");
+ form.setAction("/login");
+ form.setEnctype("multipart/form-data");
+ curi.getDataList(ExtractorHTMLForms.A_HTML_FORM_OBJECTS).add(form);
+
+ FormLoginProcessor p = (FormLoginProcessor) makeModule();
+ p.setLoginUsername("jdoe");
+ p.setLoginPassword("********");
+ p.setApplicableSurtPrefix("http://(com,example,)");
+
p.process(curi);
assertEquals(1, curi.getDataList(A_WARC_RESPONSE_HEADERS).size());
assertEquals("WARC-Simple-Form-Province-Status: 0,0,http://(com,example,)", curi.getDataList(A_WARC_RESPONSE_HEADERS).get(0));
@@ -66,7 +124,25 @@ public class FormLoginProcessorTest extends ProcessorTestBase {
CrawlURI submitCuri = curi.getOutLinks().toArray(new CrawlURI[0])[0];
assertEquals("http://example.com/login", submitCuri.toString());
assertEquals(FetchType.HTTP_POST, submitCuri.getFetchType());
- String queryString = (String) submitCuri.getData().get(CoreAttributeConstants.A_SUBMIT_DATA);
- assertEquals("username-form-field=jdoe&password-form-field=********", queryString);
+
+ FetchHTTPRequestSpy reqSpy = new FetchHTTPRequestSpy(
+ FetchHTTPTests.newTestFetchHttp(getClass().getName()),
+ submitCuri);
+ assertTrue(reqSpy.getRequest() instanceof BasicExecutionAwareEntityEnclosingRequest);
+ BasicExecutionAwareEntityEnclosingRequest req = (BasicExecutionAwareEntityEnclosingRequest) reqSpy.getRequest();
+ assertEquals("org.apache.http.entity.mime.MultipartFormEntity", req.getEntity().getClass().getName());
+ assertTrue(req.toString().startsWith("POST /login"));
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ req.getEntity().writeTo(baos);
+ System.out.println(StringEscapeUtils.escapeJava(baos.toString("UTF-8")));
+ // --e5XWkWxQ2EXBQAEPQ7n3yyvv9bI-8YIHok\r\nContent-Disposition: form-data; name=\"username-form-field\"\r\n\r\njdoe\r\n--e5XWkWxQ2EXBQAEPQ7n3yyvv9bI-8YIHok\r\nContent-Disposition: form-data; name=\"password-form-field\"\r\n\r\n********\r\n--e5XWkWxQ2EXBQAEPQ7n3yyvv9bI-8YIHok\r\nContent-Disposition: form-data; name=\"crazy🐒monkey\"\r\n\r\núhóh\r\n--e5XWkWxQ2EXBQAEPQ7n3yyvv9bI-8YIHok--\r\n
+ assertTrue(baos.toString("ascii").matches("--([a-zA-Z0-9_-]{30,41})\r\nContent-Disposition: form-data; name=\"username-form-field\"\r\n\r\njdoe\r\n--\\1\r\nContent-Disposition: form-data; name=\"password-form-field\"\r\n\r\n\\*\\*\\*\\*\\*\\*\\*\\*\r\n--\\1\r\nContent-Disposition: form-data; name=\"crazy🐒monkey\"\r\n\r\núhóh\r\n--\\1--\r\n"));
+ }
+
+ public void testEscapeForMultipart() {
+ assertEquals("abcd", FetchHTTPRequest.escapeForMultipart("abcd"));
+ assertEquals("abcdé", FetchHTTPRequest.escapeForMultipart("abcdé"));
+ assertEquals("abcdé😬", FetchHTTPRequest.escapeForMultipart("abcdé😬"));
}
}
diff --git a/pom.xml b/pom.xml
index b5ae17ec..1dc9d97f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -124,6 +124,11 @@ http://maven.apache.org/guides/mini/guide-m1-m2.html
httpclient4.3.6