blob: 3a76690eaa7f2efa1b478c1cd9bf03afbdb77cc1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.solr.servlet;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MultiMapSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.servlet.SolrRequestParsers.FormDataRequestParser;
import org.apache.solr.servlet.SolrRequestParsers.MultipartRequestParser;
import org.apache.solr.servlet.SolrRequestParsers.RawRequestParser;
import org.apache.solr.servlet.SolrRequestParsers.StandardRequestParser;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class SolrRequestParserTest extends SolrTestCaseJ4 {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@BeforeClass
public static void beforeClass() throws Exception {
assumeWorkingMockito();
initCore("solrconfig.xml", "schema.xml");
parser = new SolrRequestParsers( h.getCore().getSolrConfig() );
}
static SolrRequestParsers parser;
@AfterClass
public static void afterClass() {
parser = null;
}
@Test
public void testStreamBody() throws Exception
{
String body1 = "AMANAPLANPANAMA";
String body2 = "qwertasdfgzxcvb";
String body3 = "1234567890";
SolrCore core = h.getCore();
Map<String,String[]> args = new HashMap<>();
args.put( CommonParams.STREAM_BODY, new String[] {body1} );
// Make sure it got a single stream in and out ok
List<ContentStream> streams = new ArrayList<>();
SolrQueryRequest req = parser.buildRequestFrom( core, new MultiMapSolrParams( args ), streams );
assertEquals( 1, streams.size() );
assertEquals( body1, IOUtils.toString( streams.get(0).getReader() ) );
req.close();
// Now add three and make sure they come out ok
streams = new ArrayList<>();
args.put( CommonParams.STREAM_BODY, new String[] {body1,body2,body3} );
req = parser.buildRequestFrom( core, new MultiMapSolrParams( args ), streams );
assertEquals( 3, streams.size() );
ArrayList<String> input = new ArrayList<>();
ArrayList<String> output = new ArrayList<>();
input.add( body1 );
input.add( body2 );
input.add( body3 );
output.add( IOUtils.toString( streams.get(0).getReader() ) );
output.add( IOUtils.toString( streams.get(1).getReader() ) );
output.add( IOUtils.toString( streams.get(2).getReader() ) );
// sort them so the output is consistent
Collections.sort( input );
Collections.sort( output );
assertEquals( input.toString(), output.toString() );
req.close();
// set the contentType and make sure tat gets set
String ctype = "text/xxx";
streams = new ArrayList<>();
args.put( CommonParams.STREAM_CONTENTTYPE, new String[] {ctype} );
req = parser.buildRequestFrom( core, new MultiMapSolrParams( args ), streams );
for( ContentStream s : streams ) {
assertEquals( ctype, s.getContentType() );
}
req.close();
}
@Test
@SuppressWarnings({"try"})
public void testStreamURL() throws Exception
{
URL url = getClass().getResource("/README");
assertNotNull("Missing file 'README' in test-resources root folder.", url);
byte[] bytes = IOUtils.toByteArray(url);
SolrCore core = h.getCore();
Map<String,String[]> args = new HashMap<>();
args.put( CommonParams.STREAM_URL, new String[] { url.toExternalForm() } );
// Make sure it got a single stream in and out ok
List<ContentStream> streams = new ArrayList<>();
try (SolrQueryRequest req = parser.buildRequestFrom( core, new MultiMapSolrParams( args ), streams )) {
assertEquals( 1, streams.size() );
try (InputStream in = streams.get(0).getStream()) {
assertArrayEquals( bytes, IOUtils.toByteArray( in ) );
}
}
}
@Test
@SuppressWarnings({"try"})
public void testStreamFile() throws Exception
{
File file = getFile("README");
byte[] bytes = FileUtils.readFileToByteArray(file);
SolrCore core = h.getCore();
Map<String,String[]> args = new HashMap<>();
args.put( CommonParams.STREAM_FILE, new String[] { file.getAbsolutePath() } );
// Make sure it got a single stream in and out ok
List<ContentStream> streams = new ArrayList<>();
try (SolrQueryRequest req = parser.buildRequestFrom( core, new MultiMapSolrParams( args ), streams )) {
assertEquals( 1, streams.size() );
try (InputStream in = streams.get(0).getStream()) {
assertArrayEquals( bytes, IOUtils.toByteArray( in ) );
}
}
}
@Test
public void testUrlParamParsing() throws Exception
{
final String[][] teststr = new String[][] {
{ "this is simple", "this%20is%20simple" },
{ "this is simple", "this+is+simple" },
{ "\u00FC", "%C3%BC" }, // lower-case "u" with diaeresis/umlaut
{ "\u0026", "%26" }, // &
{ "", "" }, // empty
{ "\u20AC", "%E2%82%ac" } // euro, also with lowercase escapes
};
for( String[] tst : teststr ) {
SolrParams params = SolrRequestParsers.parseQueryString( "val="+tst[1] );
assertEquals( tst[0], params.get( "val" ) );
params = SolrRequestParsers.parseQueryString( "val="+tst[1]+"&" );
assertEquals( tst[0], params.get( "val" ) );
params = SolrRequestParsers.parseQueryString( "&&val="+tst[1]+"&" );
assertEquals( tst[0], params.get( "val" ) );
params = SolrRequestParsers.parseQueryString( "&&val="+tst[1]+"&&&val="+tst[1]+"&" );
assertArrayEquals(new String[]{tst[0],tst[0]}, params.getParams("val") );
}
SolrParams params = SolrRequestParsers.parseQueryString("val");
assertEquals("", params.get("val"));
params = SolrRequestParsers.parseQueryString("val&foo=bar=bar&muh&");
assertEquals("", params.get("val"));
assertEquals("bar=bar", params.get("foo"));
assertEquals("", params.get("muh"));
final String[] invalid = {
"q=h%FCllo", // non-UTF-8
"q=h\u00FCllo", // encoded string is not pure US-ASCII
"q=hallo%", // incomplete escape
"q=hallo%1", // incomplete escape
"q=hallo%XX123", // invalid digit 'X' in escape
"=hallo" // missing key
};
for (String s : invalid) {
expectThrows(SolrException.class, () -> SolrRequestParsers.parseQueryString(s));
}
}
@Test
public void testStandardParseParamsAndFillStreams() throws Exception
{
final String getParams = "qt=%C3%BC&dup=foo", postParams = "q=hello&d%75p=bar";
final byte[] postBytes = postParams.getBytes(StandardCharsets.US_ASCII);
// Set up the expected behavior
final String[] ct = new String[] {
"application/x-www-form-urlencoded",
"Application/x-www-form-urlencoded",
"application/x-www-form-urlencoded; charset=utf-8",
"application/x-www-form-urlencoded;"
};
for( String contentType : ct ) {
HttpServletRequest request = getMock("/solr/select", contentType, postBytes.length);
when(request.getMethod()).thenReturn("POST");
when(request.getQueryString()).thenReturn(getParams);
when(request.getInputStream()).thenReturn(new ByteServletInputStream(postBytes));
MultipartRequestParser multipart = new MultipartRequestParser( 2048 );
RawRequestParser raw = new RawRequestParser();
FormDataRequestParser formdata = new FormDataRequestParser( 2048 );
StandardRequestParser standard = new StandardRequestParser( multipart, raw, formdata );
SolrParams p = standard.parseParamsAndFillStreams(request, new ArrayList<ContentStream>());
assertEquals( "contentType: "+contentType, "hello", p.get("q") );
assertEquals( "contentType: "+contentType, "\u00FC", p.get("qt") );
assertArrayEquals( "contentType: "+contentType, new String[]{"foo","bar"}, p.getParams("dup") );
verify(request).getInputStream();
}
}
static class ByteServletInputStream extends ServletInputStream {
final BufferedInputStream in;
final int len;
int readCount = 0;
public ByteServletInputStream(byte[] data) {
this.len = data.length;
this.in = new BufferedInputStream(new ByteArrayInputStream(data));
}
@Override
public boolean isFinished() {
return readCount == len;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new IllegalStateException("Not supported");
}
@Override
public int read() throws IOException {
int read = in.read();
readCount += read;
return read;
}
}
@Test
public void testStandardParseParamsAndFillStreamsISO88591() throws Exception
{
final String getParams = "qt=%FC&dup=foo&ie=iso-8859-1&dup=%FC", postParams = "qt2=%FC&q=hello&d%75p=bar";
final byte[] postBytes = postParams.getBytes(StandardCharsets.US_ASCII);
final String contentType = "application/x-www-form-urlencoded; charset=iso-8859-1";
// Set up the expected behavior
HttpServletRequest request = getMock("/solr/select", contentType, postBytes.length);
when(request.getMethod()).thenReturn("POST");
when(request.getQueryString()).thenReturn(getParams);
when(request.getInputStream()).thenReturn(new ByteServletInputStream(postBytes));
MultipartRequestParser multipart = new MultipartRequestParser( 2048 );
RawRequestParser raw = new RawRequestParser();
FormDataRequestParser formdata = new FormDataRequestParser( 2048 );
StandardRequestParser standard = new StandardRequestParser( multipart, raw, formdata );
SolrParams p = standard.parseParamsAndFillStreams(request, new ArrayList<ContentStream>());
assertEquals( "contentType: "+contentType, "hello", p.get("q") );
assertEquals( "contentType: "+contentType, "\u00FC", p.get("qt") );
assertEquals( "contentType: "+contentType, "\u00FC", p.get("qt2") );
assertArrayEquals( "contentType: "+contentType, new String[]{"foo","\u00FC","bar"}, p.getParams("dup") );
verify(request).getInputStream();
}
@Test
public void testStandardFormdataUploadLimit() throws Exception
{
final int limitKBytes = 128;
final StringBuilder large = new StringBuilder("q=hello");
// grow exponentially to reach 128 KB limit:
while (large.length() <= limitKBytes * 1024) {
large.append('&').append(large);
}
HttpServletRequest request = getMock("/solr/select", "application/x-www-form-urlencoded", -1);
when(request.getMethod()).thenReturn("POST");
when(request.getInputStream()).thenReturn(new ByteServletInputStream(large.toString().getBytes(StandardCharsets.US_ASCII)));
FormDataRequestParser formdata = new FormDataRequestParser( limitKBytes );
SolrException e = expectThrows(SolrException.class, () -> {
formdata.parseParamsAndFillStreams(request, new ArrayList<>());
});
assertTrue(e.getMessage().contains("upload limit"));
assertEquals(400, e.code());
verify(request).getInputStream();
}
@Test
public void testParameterIncompatibilityException1() throws Exception
{
HttpServletRequest request = getMock("/solr/select", "application/x-www-form-urlencoded", 100);
// we emulate Jetty that returns empty stream when parameters were parsed before:
when(request.getInputStream()).thenReturn(new ServletInputStream() {
@Override public int read() { return -1; }
@Override
public boolean isFinished() {
return true;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
});
FormDataRequestParser formdata = new FormDataRequestParser( 2048 );
SolrException e = expectThrows(SolrException.class, () -> {
formdata.parseParamsAndFillStreams(request, new ArrayList<>());
});
assertTrue(e.getMessage().startsWith("Solr requires that request parameters"));
assertEquals(500, e.code());
verify(request).getInputStream();
}
@Test
public void testParameterIncompatibilityException2() throws Exception
{
HttpServletRequest request = getMock("/solr/select", "application/x-www-form-urlencoded", 100);
when(request.getMethod()).thenReturn("POST");
// we emulate Tomcat that throws IllegalStateException when parameters were parsed before:
when(request.getInputStream()).thenThrow(new IllegalStateException());
FormDataRequestParser formdata = new FormDataRequestParser( 2048 );
SolrException e = expectThrows(SolrException.class, () -> {
formdata.parseParamsAndFillStreams(request, new ArrayList<>());
});
assertTrue(e.getMessage().startsWith("Solr requires that request parameters"));
assertEquals(500, e.code());
verify(request).getInputStream();
}
@Test
public void testAddHttpRequestToContext() throws Exception {
HttpServletRequest request = getMock("/solr/select", null, -1);
when(request.getMethod()).thenReturn("GET");
when(request.getQueryString()).thenReturn("q=title:solr");
Map<String, String> headers = new HashMap<>();
headers.put("X-Forwarded-For", "10.0.0.1");
when(request.getHeaderNames()).thenReturn(new Vector<>(headers.keySet()).elements());
for(Map.Entry<String,String> entry:headers.entrySet()) {
Vector<String> v = new Vector<>();
v.add(entry.getValue());
when(request.getHeaders(entry.getKey())).thenReturn(v.elements());
}
SolrRequestParsers parsers = new SolrRequestParsers(h.getCore().getSolrConfig());
assertFalse(parsers.isAddRequestHeadersToContext());
SolrQueryRequest solrReq = parsers.parse(h.getCore(), "/select", request);
assertFalse(solrReq.getContext().containsKey("httpRequest"));
parsers.setAddRequestHeadersToContext(true);
solrReq = parsers.parse(h.getCore(), "/select", request);
assertEquals(request, solrReq.getContext().get("httpRequest"));
assertEquals("10.0.0.1", ((HttpServletRequest)solrReq.getContext().get("httpRequest")).getHeaders("X-Forwarded-For").nextElement());
}
public void testPostMissingContentType() throws Exception {
HttpServletRequest request = getMock();
when(request.getMethod()).thenReturn("POST");
SolrRequestParsers parsers = new SolrRequestParsers(h.getCore().getSolrConfig());
try {
parsers.parse(h.getCore(), "/select", request);
} catch (SolrException e) {
log.error("should not throw SolrException", e);
fail("should not throw SolrException");
}
}
@Test
public void testAutoDetect() throws Exception {
String curl = "curl/7.30.0";
for (String method : new String[]{"GET","POST"}) {
doAutoDetect(null, method, "{}=a", null, "{}", "a"); // unknown agent should not auto-detect
doAutoDetect(curl, method, "{}", "application/json", null, null); // curl should auto-detect
doAutoDetect(curl, method, " \t\n\r {} ", "application/json", null, null); // starting with whitespace
doAutoDetect(curl, method, " \t\n\r // how now brown cow\n {} ", "application/json", null, null); // supporting comments
doAutoDetect(curl, method, " \t\n\r #different style comment\n {} ", "application/json", null, null);
doAutoDetect(curl, method, " \t\n\r /* C style comment */\n {} ", "application/json", null, null);
doAutoDetect(curl, method, " \t\n\r <tag>hi</tag> ", "text/xml", null, null);
doAutoDetect(curl, method, " \t\r\n aaa=1&bbb=2&ccc=3", null, "bbb", "2"); // params with whitespace first
doAutoDetect(curl, method, "/x=foo&aaa=1&bbb=2&ccc=3", null, "/x", "foo"); // param name that looks like a path
doAutoDetect(curl, method, " \t\r\n /x=foo&aaa=1&bbb=2&ccc=3", null, "bbb", "2"); // param name that looks like a path
}
}
public void doAutoDetect(String userAgent, String method, final String body, String expectedContentType, String expectedKey, String expectedValue) throws Exception {
String uri = "/solr/select";
String contentType = "application/x-www-form-urlencoded";
int contentLength = -1; // does this mean auto-detect?
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getHeader("User-Agent")).thenReturn(userAgent);
when(request.getRequestURI()).thenReturn(uri);
when(request.getContentType()).thenReturn(contentType);
when(request.getContentLength()).thenReturn(contentLength);
when(request.getMethod()).thenReturn(method);
// we dont pass a content-length to let the security mechanism limit it:
when(request.getQueryString()).thenReturn("foo=1&bar=2");
when(request.getInputStream()).thenReturn(new ByteServletInputStream(body.getBytes(StandardCharsets.US_ASCII)));
SolrRequestParsers parsers = new SolrRequestParsers(h.getCore().getSolrConfig());
SolrQueryRequest req = parsers.parse(h.getCore(), "/select", request);
int num=0;
if (expectedContentType != null) {
for (ContentStream cs : req.getContentStreams()) {
num++;
assertTrue(cs.getContentType().startsWith(expectedContentType));
String returnedBody = IOUtils.toString(cs.getReader());
assertEquals(body, returnedBody);
}
assertEquals(1, num);
}
assertEquals("1", req.getParams().get("foo"));
assertEquals("2", req.getParams().get("bar"));
if (expectedKey != null) {
assertEquals(expectedValue, req.getParams().get(expectedKey));
}
req.close();
verify(request).getInputStream();
}
public HttpServletRequest getMock() {
return getMock("/solr/select", null, -1);
}
public HttpServletRequest getMock(String uri, String contentType, int contentLength) {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getRequestURI()).thenReturn(uri);
when(request.getContentType()).thenReturn(contentType);
when(request.getContentLength()).thenReturn(contentLength);
return request;
}
}