blob: 4b5834676703c659dea27924664f10bed59d85dc [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.test.rfcs;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.TimeZone;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.test.support.StringUtil;
import org.eclipse.jetty.test.support.TestableJettyServer;
import org.eclipse.jetty.test.support.rawhttp.HttpSocket;
import org.eclipse.jetty.test.support.rawhttp.HttpTesting;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.StringAssert;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.hamcrest.Matchers;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
/**
* <a href="http://tools.ietf.org/html/rfc2616">RFC 2616</a> (HTTP/1.1) Test Case
*/
public abstract class RFC2616BaseTest
{
private static final String ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n";
/** STRICT RFC TESTS */
private static final boolean STRICT = false;
private static TestableJettyServer server;
private HttpTesting http;
class TestFile
{
String name;
String modDate;
String data;
long length;
public TestFile(String name)
{
this.name = name;
// HTTP-Date format - see RFC 2616 section 14.29
SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
this.modDate = sdf.format(new Date());
}
public void setData(String data)
{
this.data = data;
this.length = data.length();
}
}
public static void setUpServer(TestableJettyServer testableserver, Class<?> testclazz) throws Exception
{
File testWorkDir = MavenTestingUtils.getTargetTestingDir(testclazz.getName());
FS.ensureDirExists(testWorkDir);
System.setProperty("java.io.tmpdir",testWorkDir.getAbsolutePath());
server = testableserver;
server.load();
server.start();
//server.getServer().dumpStdErr();
}
@Before
public void setUp() throws Exception
{
http = new HttpTesting(getHttpClientSocket(),server.getServerPort());
}
@AfterClass
public static void tearDownServer() throws Exception
{
server.stop();
}
public abstract HttpSocket getHttpClientSocket() throws Exception;
/**
* Test Date/Time format Specs.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-3.3">RFC 2616 (section 3.3)</a>
*/
@Test
public void test3_3()
{
Calendar expected = Calendar.getInstance();
expected.set(Calendar.YEAR,1994);
expected.set(Calendar.MONTH,Calendar.NOVEMBER);
expected.set(Calendar.DAY_OF_MONTH,6);
expected.set(Calendar.HOUR_OF_DAY,8);
expected.set(Calendar.MINUTE,49);
expected.set(Calendar.SECOND,37);
expected.set(Calendar.MILLISECOND,0); // Milliseconds is not compared
expected.set(Calendar.ZONE_OFFSET,0); // Use GMT+0:00
expected.set(Calendar.DST_OFFSET,0); // No Daylight Savings Offset
HttpFields fields = new HttpFields();
// RFC 822 Preferred Format
fields.put("D1","Sun, 6 Nov 1994 08:49:37 GMT");
// RFC 822 / RFC 850 Format
fields.put("D2","Sunday, 6-Nov-94 08:49:37 GMT");
// RFC 850 / ANSIC C Format
fields.put("D3","Sun Nov 6 08:49:37 1994");
// Test parsing
assertDate("3.3.1 RFC 822 Preferred",expected,fields.getDateField("D1"));
assertDate("3.3.1 RFC 822 / RFC 850",expected,fields.getDateField("D2"));
assertDate("3.3.1 RFC 850 / ANSI C",expected,fields.getDateField("D3"));
// Test formatting
fields.putDateField("Date",expected.getTime().getTime());
Assert.assertEquals("3.3.1 RFC 822 preferred","Sun, 06 Nov 1994 08:49:37 GMT",fields.get("Date"));
}
/**
* Test Transfer Codings
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-3.6">RFC 2616 (section 3.6)</a>
*/
@Test
public void test3_6() throws Throwable
{
// Chunk last
StringBuffer req1 = new StringBuffer();
req1.append("GET /tests/R1 HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Transfer-Encoding: chunked,identity\n"); // Invalid Transfer-Encoding
req1.append("Content-Type: text/plain\n");
req1.append("Connection: close\n");
req1.append("\r\n");
req1.append("5;\r\n");
req1.append("123\r\n\r\n");
req1.append("0;\r\n\r\n");
HttpTester.Response response = http.request(req1);
assertEquals("3.6 Transfer Coding / Bad 400",HttpStatus.BAD_REQUEST_400,response.getStatus());
}
/**
* Test Transfer Codings
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-3.6">RFC 2616 (section 3.6)</a>
*/
@Test
public void test3_6_2() throws Throwable
{
// Chunked
StringBuffer req2 = new StringBuffer();
req2.append("GET /echo/R1 HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Transfer-Encoding: chunked\n");
req2.append("Content-Type: text/plain\n");
req2.append("\n");
req2.append("2;\n"); // 2 chars
req2.append("12\n");
req2.append("3;\n"); // 3 chars
req2.append("345\n");
req2.append("0;\n\n");
req2.append("GET /echo/R2 HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Transfer-Encoding: chunked\n");
req2.append("Content-Type: text/plain\n");
req2.append("\n");
req2.append("4;\n"); // 4 chars
req2.append("6789\n");
req2.append("5;\n"); // 5 chars
req2.append("abcde\n");
req2.append("0;\n\n"); // 0 chars
req2.append("GET /echo/R3 HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Connection: close\n");
req2.append("\n");
List<HttpTester.Response> responses = http.requests(req2);
Assert.assertEquals("Response Count",3,responses.size());
HttpTester.Response response = responses.get(0); // Response 1
assertEquals("3.6.1 Transfer Codings / Response 1 Code", HttpStatus.OK_200, response.getStatus());
assertTrue("3.6.1 Transfer Codings / Chunked String", response.getContent().contains("12345\n"));
response = responses.get(1); // Response 2
assertEquals("3.6.1 Transfer Codings / Response 2 Code", HttpStatus.OK_200, response.getStatus());
assertThat("3.6.1 Transfer Codings / Chunked String",response.getContent(),Matchers.containsString("6789abcde\n"));
response = responses.get(2); // Response 3
assertEquals("3.6.1 Transfer Codings / Response 3 Code", HttpStatus.OK_200, response.getStatus());
assertEquals("3.6.1 Transfer Codings / No Body","",response.getContent());
}
/**
* Test Transfer Codings
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-3.6">RFC 2616 (section 3.6)</a>
*/
@Test
public void test3_6_3() throws Throwable
{
// Chunked
StringBuffer req3 = new StringBuffer();
req3.append("POST /echo/R1 HTTP/1.1\n");
req3.append("Host: localhost\n");
req3.append("Transfer-Encoding: chunked\n");
req3.append("Content-Type: text/plain\n");
req3.append("\n");
req3.append("3;\n"); // 3 chars
req3.append("fgh\n");
req3.append("3;\n"); // 3 chars
req3.append("Ijk\n");
req3.append("0;\n\n"); // 0 chars
req3.append("POST /echo/R2 HTTP/1.1\n");
req3.append("Host: localhost\n");
req3.append("Transfer-Encoding: chunked\n");
req3.append("Content-Type: text/plain\n");
req3.append("\n");
req3.append("4;\n"); // 4 chars
req3.append("lmno\n");
req3.append("5;\n"); // 5 chars
req3.append("Pqrst\n");
req3.append("0;\n\n"); // 0 chars
req3.append("GET /echo/R3 HTTP/1.1\n");
req3.append("Host: localhost\n");
req3.append("Connection: close\n");
req3.append("\n");
List<HttpTester.Response> responses = http.requests(req3);
Assert.assertEquals("Response Count",3,responses.size());
HttpTester.Response response = responses.get(0); // Response 1
assertEquals("3.6.1 Transfer Codings / Response 1 Code", HttpStatus.OK_200, response.getStatus());
assertTrue("3.6.1 Transfer Codings / Chunked String", response.getContent().contains("fghIjk\n")); // Complete R1 string
response = responses.get(1); // Response 2
assertEquals("3.6.1 Transfer Codings / Response 2 Code", HttpStatus.OK_200, response.getStatus());
assertTrue("3.6.1 Transfer Codings / Chunked String", response.getContent().contains("lmnoPqrst\n")); // Complete R2 string
response = responses.get(2); // Response 3
assertEquals("3.6.1 Transfer Codings / Response 3 Code", HttpStatus.OK_200, response.getStatus());
assertEquals("3.6.1 Transfer Codings / No Body","",response.getContent());
}
/**
* Test Transfer Codings
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-3.6">RFC 2616 (section 3.6)</a>
*/
@Test
public void test3_6_4() throws Throwable
{
// Chunked and keep alive
StringBuffer req4 = new StringBuffer();
req4.append("GET /echo/R1 HTTP/1.1\n");
req4.append("Host: localhost\n");
req4.append("Transfer-Encoding: chunked\n");
req4.append("Content-Type: text/plain\n");
req4.append("Connection: keep-alive\n"); // keep-alive
req4.append("\n");
req4.append("3;\n"); // 3 chars
req4.append("123\n");
req4.append("3;\n"); // 3 chars
req4.append("456\n");
req4.append("0;\n\n"); // 0 chars
req4.append("GET /echo/R2 HTTP/1.1\n");
req4.append("Host: localhost\n");
req4.append("Connection: close\n"); // close
req4.append("\n");
List<HttpTester.Response> responses = http.requests(req4);
Assert.assertEquals("Response Count",2,responses.size());
HttpTester.Response response = responses.get(0); // Response 1
assertEquals("3.6.1 Transfer Codings / Response 1 Code", HttpStatus.OK_200, response.getStatus());
assertTrue("3.6.1 Transfer Codings / Chunked String", response.getContent().contains("123456\n")); // Complete R1 string
response = responses.get(1); // Response 2
assertEquals("3.6.1 Transfer Codings / Response 2 Code", HttpStatus.OK_200, response.getStatus());
assertEquals("3.6.1 Transfer Codings / No Body","",response.getContent());
}
/**
* Test Quality Values
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-3.9">RFC 2616 (section 3.9)</a>
*/
@Test
public void test3_9()
{
HttpFields fields = new HttpFields();
fields.put("Q","bbb;q=0.5,aaa,ccc;q=0.002,d;q=0,e;q=0.0001,ddd;q=0.001,aa2,abb;q=0.7");
Enumeration<String> qualities = fields.getValues("Q",", \t");
List<?> list = HttpFields.qualityList(qualities);
Assert.assertEquals("Quality parameters","aaa",HttpFields.valueParameters(list.get(0).toString(),null));
Assert.assertEquals("Quality parameters","aa2",HttpFields.valueParameters(list.get(1).toString(),null));
Assert.assertEquals("Quality parameters","abb",HttpFields.valueParameters(list.get(2).toString(),null));
Assert.assertEquals("Quality parameters","bbb",HttpFields.valueParameters(list.get(3).toString(),null));
Assert.assertEquals("Quality parameters","ccc",HttpFields.valueParameters(list.get(4).toString(),null));
Assert.assertEquals("Quality parameters","ddd",HttpFields.valueParameters(list.get(5).toString(),null));
}
/**
* Test Message Length
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-4.4">RFC 2616 (section 4.4)</a>
*/
@Test
public void test4_4() throws Exception
{
// 4.4.2 - transfer length is 'chunked' when the 'Transfer-Encoding' header
// is provided with a value other than 'identity', unless the
// request message is terminated with a 'Connection: close'.
StringBuffer req1 = new StringBuffer();
req1.append("GET /echo/R1 HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Transfer-Encoding: identity\n");
req1.append("Content-Type: text/plain\n");
req1.append("Content-Length: 5\n");
req1.append("\n");
req1.append("123\r\n");
req1.append("GET /echo/R2 HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Connection: close\n");
req1.append("\n");
List<HttpTester.Response> responses = http.requests(req1);
Assert.assertEquals("Response Count",2,responses.size());
HttpTester.Response response = responses.get(0);
assertEquals("4.4.2 Message Length / Response Code", HttpStatus.OK_200, response.getStatus());
assertThat("4.4.2 Message Length / Body",response.getContent(),Matchers.containsString("123\n"));
response = responses.get(1);
assertEquals("4.4.2 Message Length / Response Code", HttpStatus.OK_200, response.getStatus());
assertEquals("4.4.2 Message Length / No Body", "",response.getContent());
// 4.4.3 -
// Client - do not send 'Content-Length' if entity-length
// and the transfer-length are different.
// Server - ignore 'Content-Length' if 'Transfer-Encoding' is provided.
StringBuffer req2 = new StringBuffer();
req2.append("GET /echo/R1 HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Transfer-Encoding: chunked\n");
req2.append("Content-Type: text/plain\n");
req2.append("Content-Length: 100\n");
req2.append("\n");
req2.append("3;\n");
req2.append("123\n");
req2.append("3;\n");
req2.append("456\n");
req2.append("0;\n");
req2.append("\n");
req2.append("GET /echo/R2 HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Connection: close\n");
req2.append("Content-Type: text/plain\n");
req2.append("Content-Length: 6\n");
req2.append("\n");
req2.append("7890AB");
responses = http.requests(req2);
Assert.assertEquals("Response Count",2,responses.size());
response = responses.get(0); // response 1
assertEquals("4.4.3 Ignore Content-Length / Response Code", HttpStatus.OK_200, response.getStatus());
assertTrue("4.4.3 Ignore Content-Length / Body", response.getContent().contains("123456\n"));
response = responses.get(1); // response 2
assertEquals("4.4.3 Ignore Content-Length / Response Code", HttpStatus.OK_200, response.getStatus());
assertTrue("4.4.3 Ignore Content-Length / Body", response.getContent().contains("7890AB\n"));
// 4.4 - Server can request valid Content-Length from client if client
// fails to provide a Content-Length.
// Server can respond with 400 (Bad Request) or 411 (Length Required).
// NOTE: MSIE breaks this rule
// TODO: Document which version of MSIE Breaks Rule.
// TODO: Document which versions of MSIE pass this rule (if any).
if (STRICT)
{
StringBuffer req3 = new StringBuffer();
req3.append("GET /echo/R2 HTTP/1.1\n");
req3.append("Host: localhost\n");
req3.append("Content-Type: text/plain\n");
req3.append("Connection: close\n");
req3.append("\n");
req3.append("123456");
response = http.request(req3);
assertEquals("4.4 Valid Content-Length Required",HttpStatus.LENGTH_REQUIRED_411, response.getStatus());
assertTrue("4.4 Valid Content-Length Required", response.getContent() == null);
StringBuffer req4 = new StringBuffer();
req4.append("GET /echo/R2 HTTP/1.0\n");
req4.append("Content-Type: text/plain\n");
req4.append("\n");
req4.append("123456");
response = http.request(req4);
assertEquals("4.4 Valid Content-Length Required",HttpStatus.LENGTH_REQUIRED_411, response.getStatus());
assertTrue("4.4 Valid Content-Length Required", response.getContent() == null);
}
}
/**
* Test The Resource Identified by a Request
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-5.2">RFC 2616 (section 5.2)</a>
*/
@Test
public void test5_2_DefaultHost() throws Exception
{
// Default Host
StringBuffer req1 = new StringBuffer();
req1.append("GET /tests/index.html HTTP/1.1\n");
req1.append("Host: localhost\n"); // default host
req1.append("Connection: close\n");
req1.append("\r\n");
HttpTester.Response response = http.request(req1);
assertEquals("5.2 Default Host", HttpStatus.OK_200, response.getStatus());
assertThat("5.2 Default Host",response.getContent(),Matchers.containsString("Default DOCRoot"));
}
/**
* Test The Resource Identified by a Request
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-5.2">RFC 2616 (section 5.2)</a>
*/
@Test
public void test5_2_VirtualHost() throws Exception
{
// Virtual Host
StringBuffer req2 = new StringBuffer();
req2.append("GET /tests/ HTTP/1.1\n");
req2.append("Host: VirtualHost\n"); // simple virtual host
req2.append("Connection: close\n");
req2.append("\r\n");
HttpTester.Response response = http.request(req2);
assertEquals("5.2 Virtual Host", HttpStatus.OK_200, response.getStatus());
assertThat("5.2 Virtual Host",response.getContent(),Matchers.containsString("VirtualHost DOCRoot"));
}
/**
* Test The Resource Identified by a Request
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-5.2">RFC 2616 (section 5.2)</a>
*/
@Test
public void test5_2_VirtualHostInsensitive() throws Exception
{
// Virtual Host case insensitive
StringBuffer req3 = new StringBuffer();
req3.append("GET /tests/ HTTP/1.1\n");
req3.append("Host: ViRtUalhOst\n"); // mixed case host
req3.append("Connection: close\n");
req3.append("\n");
HttpTester.Response response = http.request(req3);
assertEquals("5.2 Virtual Host (mixed case)", HttpStatus.OK_200, response.getStatus());
assertThat("5.2 Virtual Host (mixed case)",response.getContent(),Matchers.containsString("VirtualHost DOCRoot"));
}
/**
* Test The Resource Identified by a Request
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-5.2">RFC 2616 (section 5.2)</a>
*/
@Test
public void test5_2_NoVirtualHost() throws Exception
{
// No Virtual Host
StringBuffer req4 = new StringBuffer();
req4.append("GET /tests/ HTTP/1.1\n");
req4.append("Connection: close\n");
req4.append("\n"); // no virtual host
HttpTester.Response response = http.request(req4);
assertEquals("5.2 No Host",HttpStatus.BAD_REQUEST_400,response.getStatus());
assertEquals("5.2 No Host","", response.getContent());
}
/**
* Test The Resource Identified by a Request
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-5.2">RFC 2616 (section 5.2)</a>
*/
@Test
public void test5_2_BadVirtualHost() throws Exception
{
// Bad Virtual Host
StringBuffer req5 = new StringBuffer();
req5.append("GET /tests/ HTTP/1.1\n");
req5.append("Host: bad.eclipse.org\n"); // Bad virtual host
req5.append("Connection: close\n");
req5.append("\n");
HttpTester.Response response = http.request(req5);
assertEquals("5.2 Bad Host",HttpStatus.OK_200, response.getStatus());
assertThat("5.2 Bad Host",response.getContent(),Matchers.containsString("Default DOCRoot")); // served by default context
}
/**
* Test The Resource Identified by a Request
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-5.2">RFC 2616 (section 5.2)</a>
*/
@Test
public void test5_2_VirtualHostAbsoluteURI_Http11_WithoutHostHeader() throws Exception
{
// Virtual Host as Absolute URI
StringBuffer req6 = new StringBuffer();
req6.append("GET http://VirtualHost/tests/ HTTP/1.1\n");
req6.append("Connection: close\n");
req6.append("\n");
HttpTester.Response response = http.request(req6);
// No host header should always return a 400 Bad Request by 19.6.1.1
assertEquals("5.2 Virtual Host as AbsoluteURI (No Host Header / HTTP 1.1)",HttpStatus.BAD_REQUEST_400,response.getStatus());
}
/**
* Test The Resource Identified by a Request
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-5.2">RFC 2616 (section 5.2)</a>
*/
@Test
public void test5_2_VirtualHostAbsoluteURI_Http10_WithoutHostHeader() throws Exception
{
// Virtual Host as Absolute URI
StringBuffer req6 = new StringBuffer();
req6.append("GET http://VirtualHost/tests/ HTTP/1.0\n");
req6.append("Connection: close\n");
req6.append("\n");
HttpTester.Response response = http.request(req6);
assertEquals("5.2 Virtual Host as AbsoluteURI (No Host Header / HTTP 1.0)",HttpStatus.OK_200, response.getStatus());
assertThat("5.2 Virtual Host as AbsoluteURI (No Host Header / HTTP 1.1)",response.getContent(),Matchers.containsString("VirtualHost DOCRoot"));
}
/**
* Test The Resource Identified by a Request
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-5.2">RFC 2616 (section 5.2)</a>
*/
@Test
public void test5_2_VirtualHostAbsoluteURI_WithHostHeader() throws Exception
{
// Virtual Host as Absolute URI (with Host header)
StringBuffer req7 = new StringBuffer();
req7.append("GET http://VirtualHost/tests/ HTTP/1.1\n");
req7.append("Host: localhost\n"); // is ignored (would normally trigger default context)
req7.append("Connection: close\n");
req7.append("\n");
HttpTester.Response response = http.request(req7);
assertEquals("5.2 Virtual Host as AbsoluteURI (and Host header)", HttpStatus.OK_200, response.getStatus());
// System.err.println(response.getContent());
assertThat("5.2 Virtual Host as AbsoluteURI (and Host header)",response.getContent(),Matchers.containsString("VirtualHost DOCRoot"));
}
/**
* Test Persistent Connections
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-8.1">RFC 2616 (section 8.1)</a>
*/
@Test
public void test8_1() throws Exception
{
StringBuffer req1 = new StringBuffer();
req1.append("GET /tests/R1.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
assertEquals("8.1 Persistent Connections", HttpStatus.OK_200, response.getStatus());
assertTrue("8.1 Persistent Connections", response.get("Content-Length") != null);
assertThat("8.1 Persistent Connections",response.getContent(),Matchers.containsString("Resource=R1"));
StringBuffer req2 = new StringBuffer();
req2.append("GET /tests/R1.txt HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("\n");
req2.append("GET /tests/R2.txt HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Connection: close\n");
req2.append("\n");
req2.append("GET /tests/R3.txt HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Connection: close\n");
req2.append("\n");
List<HttpTester.Response> responses = http.requests(req2);
Assert.assertEquals("Response Count",2,responses.size()); // Should not have a R3 response.
response = responses.get(0); // response 1
assertEquals("8.1 Persistent Connections", HttpStatus.OK_200, response.getStatus());
assertTrue("8.1 Persistent Connections",response.get("Content-Length") != null);
assertTrue("8.1 Peristent Connections", response.getContent().contains("Resource=R1"));
response = responses.get(1); // response 2
assertEquals("8.1.2.2 Persistent Connections / Pipeline", HttpStatus.OK_200, response.getStatus());
assertTrue("8.1.2.2 Persistent Connections / Pipeline", response.get("Content-Length") != null);
assertEquals("8.1.2.2 Persistent Connections / Pipeline","close", response.get("Connection"));
assertTrue("8.1.2.2 Peristent Connections / Pipeline", response.getContent().contains("Resource=R2"));
}
/**
* Test Message Transmission Requirements -- Bad client behaviour, invalid Expect header.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-8.2">RFC 2616 (section 8.2)</a>
*/
@Test
public void test8_2_ExpectInvalid() throws Exception
{
// Expect Failure
StringBuffer req2 = new StringBuffer();
req2.append("GET /echo/R1 HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Expect: unknown\n"); // Invalid Expect header.
req2.append("Content-Type: text/plain\n");
req2.append("Content-Length: 8\n");
req2.append("\n");
req2.append("12345678\n");
HttpTester.Response response = http.request(req2);
assertEquals("8.2.3 expect failure",HttpStatus.EXPECTATION_FAILED_417, response.getStatus());
}
/**
* Test Message Transmission Requirements -- Acceptable bad client behavior, Expect 100 with body content.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-8.2">RFC 2616 (section 8.2)</a>
*/
@Test
public void test8_2_ExpectWithBody() throws Exception
{
// Expect with body
StringBuffer req3 = new StringBuffer();
req3.append("GET /echo/R1 HTTP/1.1\n");
req3.append("Host: localhost\n");
req3.append("Expect: 100-continue\n"); // Valid Expect header.
req3.append("Content-Type: text/plain\n");
req3.append("Content-Length: 8\n");
req3.append("Connection: close\n");
req3.append("\n");
req3.append("123456\r\n"); // Body
// Should only expect 1 response.
// The existence of 2 responses usually means a bad "HTTP/1.1 100" was received.
HttpTester.Response response = http.request(req3);
assertEquals("8.2.3 expect 100", HttpStatus.OK_200, response.getStatus());
}
/**
* Test Message Transmission Requirements -- Acceptable bad client behavior, Expect 100 with body content.
* @throws Exception failure
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-8.2">RFC 2616 (section 8.2)</a>
*/
@Test
public void test8_2_UnexpectWithBody() throws Exception
{
// Expect with body
StringBuffer req3 = new StringBuffer();
req3.append("GET /redirect/R1 HTTP/1.1\n");
req3.append("Host: localhost\n");
req3.append("Expect: 100-continue\n"); // Valid Expect header.
req3.append("Content-Type: text/plain\n");
req3.append("Content-Length: 8\n");
req3.append("\n");
req3.append("123456\r\n");
req3.append("GET /echo/R1 HTTP/1.1\n");
req3.append("Host: localhost\n");
req3.append("Content-Type: text/plain\n");
req3.append("Content-Length: 8\n");
req3.append("Connection: close\n");
req3.append("\n");
req3.append("87654321"); // Body
List<HttpTester.Response> responses = http.requests(req3);
// System.err.println(responses);
HttpTester.Response response=responses.get(0);
// System.err.println(response);
assertEquals("8.2.3 ignored no 100",302, response.getStatus());
assertEquals("close",response.get("Connection"));
assertEquals(1,responses.size());
}
/**
* Test Message Transmission Requirements
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-8.2">RFC 2616 (section 8.2)</a>
*/
@Test
public void test8_2_ExpectNormal() throws Exception
{
// Expect 100
StringBuffer req4 = new StringBuffer();
req4.append("GET /echo/R1 HTTP/1.1\n");
req4.append("Host: localhost\n");
req4.append("Connection: close\n");
req4.append("Expect: 100-continue\n"); // Valid Expect header.
req4.append("Content-Type: text/plain\n");
req4.append("Content-Length: 7\n");
req4.append("\n"); // No body
Socket sock = http.open();
try
{
http.send(sock,req4);
http.setTimeoutMillis(2000);
HttpTester.Response response = http.readAvailable(sock);
assertEquals("8.2.3 expect 100",HttpStatus.CONTINUE_100,response.getStatus());
http.send(sock,"654321\n"); // Now send the data
response = http.read(sock);
assertEquals("8.2.3 expect 100", HttpStatus.OK_200, response.getStatus());
assertThat("8.2.3 expect 100",response.getContent(),Matchers.containsString("654321\n"));
}
finally
{
http.close(sock);
}
}
/**
* Test OPTIONS (HTTP) method - Server Options
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-9.2">RFC 2616 (section 9.2)</a>
*/
@Test
public void test9_2_ServerOptions() throws Exception
{
// Unsupported in Jetty.
// Server can handle many webapps, each with their own set of supported OPTIONS.
// Both www.cnn.com and www.apache.org do NOT support this request as well.
if (STRICT)
{
// Server OPTIONS
StringBuffer req1 = new StringBuffer();
req1.append("OPTIONS * HTTP/1.1\n"); // Apply to server in general, rather than a specific resource
req1.append("Connection: close\n");
req1.append("Host: localhost\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
assertEquals("9.2 OPTIONS", HttpStatus.OK_200, response.getStatus());
assertTrue("9.2 OPTIONS",response.get("Allow") != null);
// Header expected ...
// Allow: GET, HEAD, POST, PUT, DELETE, MOVE, OPTIONS, TRACE
String allow = response.get("Allow");
String expectedMethods[] =
{ "GET", "HEAD", "POST", "PUT", "DELETE", "MOVE", "OPTIONS", "TRACE" };
for (String expectedMethod : expectedMethods)
{
assertThat(allow,containsString(expectedMethod));
}
assertEquals("9.2 OPTIONS","0", response.get("Content-Length")); // Required if no response body.
}
}
/**
* Test OPTIONS (HTTP) method - Resource Options
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-9.2">RFC 2616 (section 9.2)</a>
*/
@Test
public void test9_2_ResourceOptions() throws Exception
{
// Jetty is conditionally compliant.
// Possible Bug in the Spec.
// The content-length: 0 in the spec is not appropriate if the connection is being closed.
// Resource specific OPTIONS
StringBuffer req2 = new StringBuffer();
req2.append("OPTIONS /rfc2616-webapp/httpmethods HTTP/1.1\n"); // Apply to specific resource
req2.append("Host: localhost\n");
req2.append("\n");
// Test issues 2 requests. first as OPTIONS (not closed),
// second as GET (closed), this is to allow the 2 conflicting aspects of the
// RFC2616 rules with regards to section 9.2 (OPTIONS) and section 4.4 (Message Length)
// to not conflict with each other.
req2.append("GET /rfc2616-webapp/httpmethods HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Connection: close\n"); // Close this second request
req2.append("\n");
List<HttpTester.Response> responses = http.requests(req2);
Assert.assertEquals("Response Count",2,responses.size()); // Should have 2 responses
HttpTester.Response response = responses.get(0); // Only interested in first response
assertTrue("9.2 OPTIONS", response.get("Allow") != null);
// Header expected ...
// Allow: GET, HEAD, POST, TRACE, OPTIONS
String allow = response.get("Allow");
String expectedMethods[] =
{ "GET", "HEAD", "POST", "OPTIONS", "TRACE" };
for (String expectedMethod : expectedMethods)
{
assertThat(allow,containsString(expectedMethod));
}
assertEquals("9.2 OPTIONS","0", response.get("Content-Length")); // Required if no response body.
}
/**
* Test HEAD (HTTP) method
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-9.4">RFC 2616 (section 9.4)</a>
*/
@Test
public void test9_4() throws Exception
{
/* Test GET first. (should have body) */
StringBuffer req1 = new StringBuffer();
req1.append("GET /tests/R1.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
assertEquals("9.4 GET / Response Code", HttpStatus.OK_200, response.getStatus());
assertEquals("9.4 GET / Content Type","text/plain", response.get("Content-Type"));
assertEquals("9.4 HEAD / Content Type","25", response.get("Content-Length"));
assertTrue("9.4 GET / Body", response.getContent().contains("Host=Default\nResource=R1\n"));
/* Test HEAD next. (should have no body) */
StringBuffer req2 = new StringBuffer();
req2.append("HEAD /tests/R1.txt HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Connection: close\n");
req2.append("\n");
// Need to get the HEAD response in a RAW format, as HttpParser.parse()
// can't properly parse a HEAD response.
Socket sock = http.open();
try
{
http.send(sock,req2);
String rawHeadResponse = http.readRaw(sock);
int headResponseLength = rawHeadResponse.length();
// Only interested in the response header from the GET request above.
String rawGetResponse = response.toString().substring(0,headResponseLength);
// As there is a possibility that the time between GET and HEAD requests
// can cross the second mark. (eg: GET at 11:00:00.999 and HEAD at 11:00:01.001)
// So with that knowledge, we will remove the 'Date:' header from both sides before comparing.
List<String> linesGet = StringUtil.asLines(rawGetResponse.trim());
List<String> linesHead = StringUtil.asLines(rawHeadResponse.trim());
StringUtil.removeStartsWith("Date: ",linesGet);
StringUtil.removeStartsWith("Date: ",linesHead);
// Compare the 2 lists of lines to make sure they contain the same information
// Do not worry about order of the headers, as that's not important to test,
// just the existence of the same headers
StringAssert.assertContainsSame("9.4 HEAD equals GET",linesGet,linesHead);
}
finally
{
http.close(sock);
}
}
/**
* Test TRACE (HTTP) method
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-9.8">RFC 2616 (section 9.8)</a>
*/
@Test
@Ignore("Introduction of fix for realm-less security constraints has rendered this test invalid due to default configuration preventing use of TRACE in webdefault.xml")
public void test9_8() throws Exception
{
StringBuffer req1 = new StringBuffer();
req1.append("TRACE /rfc2616-webapp/httpmethods HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
assertEquals("9.8 TRACE / Response Code", HttpStatus.OK_200, response.getStatus());
assertEquals("9.8 TRACE / Content Type", "message/http", response.get("Content-Type"));
assertTrue("9.8 TRACE / echo", response.getContent().contains("TRACE /rfc2616-webapp/httpmethods HTTP/1.1"));
assertTrue("9.8 TRACE / echo", response.getContent().contains("Host: localhost"));
}
/**
* Test 206 Partial Content (Response Code)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.2.7">RFC 2616 (section 10.2.7)</a>
*/
@Test
public void test10_2_7() throws Exception
{
// check to see if corresponding GET w/o range would return
// a) ETag
// b) Content-Location
// these same headers will be required for corresponding
// sub range requests
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
boolean noRangeHasContentLocation = (response.get("Content-Location") != null);
boolean noRangeHasETag = (response.get("ETag") != null);
// now try again for the same resource but this time WITH range header
StringBuffer req2 = new StringBuffer();
req2.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Connection: close\n");
req2.append("Range: bytes=1-3\n"); // request first 3 bytes
req2.append("\n");
response = http.request(req2);
// System.err.println(response);
assertEquals("10.2.7 Partial Content",HttpStatus.PARTIAL_CONTENT_206, response.getStatus());
// (point 1) A 206 response MUST contain either a Content-Range header
// field (section 14.16) indicating the range included with this
// response, or a multipart/byteranges Content-Type including Content-Range
// fields for each part. If a Content-Length header field is present
// in the response, its value MUST match the actual number of OCTETs
// transmitted in the message-body.
if (response.get("Content-Range") != null)
{
assertEquals("10.2.7 Partial Content / Response / Content Range","bytes 1-3/27",response.get("Content-Range"));
}
if (response.get("Content-Length") != null)
{
assertEquals("10.2.7 Patial Content / Response / Content Length","3", response.get("Content-Length"));
}
// (point 2) A 206 response MUST contain a Date header
assertTrue("10.2.7 Partial Content / Response / Date", response.get("Date") != null);
// (point 3) A 206 response MUST contain ETag and/or Content-Location,
// if the header would have been sent in a 200 response to the same request
if (noRangeHasContentLocation)
{
assertTrue("10.2.7 Partial Content / Content-Location", response.get("Content-Location") != null);
}
if (noRangeHasETag)
{
assertTrue("10.2.7 Partial Content / Content-Location", response.get("ETag") != null);
}
// (point 4) A 206 response MUST contain Expires, Cache-Control, and/or Vary,
// if the field-value might differ from that sent in any previous response
// for the same variant
// TODO: Not sure how to test this condition.
// Test the body sent
assertThat("10.2.7 Partial Content",response.getContent(),Matchers.containsString("BCD")); // should only have bytes 1-3
}
/**
* Test Redirection 3xx (Response Code)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3">RFC 2616 (section 10.3)</a>
*/
@Test
public void test10_3_RedirectHttp10Path() throws Exception
{
String specId;
String serverURI = server.getServerURI().toASCIIString();
// HTTP/1.0
StringBuffer req1 = new StringBuffer();
req1.append("GET /redirect/ HTTP/1.0\n");
req1.append("Connection: Close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
specId = "10.3 Redirection HTTP/1.0 - basic";
assertEquals(specId,HttpStatus.FOUND_302, response.getStatus());
assertEquals(specId,serverURI + "/tests/", response.get("Location"));
}
/**
* Test Redirection 3xx (Response Code)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3">RFC 2616 (section 10.3)</a>
*/
@Test
public void test10_3_RedirectHttp11Path() throws Exception
{
// HTTP/1.1
StringBuffer req2 = new StringBuffer();
req2.append("GET /redirect/ HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("\n");
req2.append("GET /redirect/ HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("Connection: close\n");
req2.append("\n");
List<HttpTester.Response> responses = http.requests(req2);
Assert.assertEquals("Response Count",2,responses.size());
HttpTester.Response response = responses.get(0);
String specId = "10.3 Redirection HTTP/1.1 - basic (response 1)";
assertEquals(specId,HttpStatus.FOUND_302, response.getStatus());
assertEquals(specId,server.getScheme() + "://localhost/tests/", response.get("Location"));
response = responses.get(1);
specId = "10.3 Redirection HTTP/1.1 - basic (response 2)";
assertEquals(specId,HttpStatus.FOUND_302, response.getStatus());
assertEquals(specId,server.getScheme() + "://localhost/tests/", response.get("Location"));
assertEquals(specId,"close", response.get("Connection"));
}
/**
* Test Redirection 3xx (Response Code)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3">RFC 2616 (section 10.3)</a>
*/
@Test
public void test10_3_RedirectHttp10Resource() throws Exception
{
// HTTP/1.0 - redirect with resource/content
StringBuffer req3 = new StringBuffer();
req3.append("GET /redirect/R1.txt HTTP/1.0\n");
req3.append("Host: localhost\n");
req3.append("Connection: close\n");
req3.append("\n");
HttpTester.Response response = http.request(req3);
String specId = "10.3 Redirection HTTP/1.0 w/content";
assertEquals(specId,HttpStatus.FOUND_302, response.getStatus());
assertEquals(specId,server.getScheme() + "://localhost/tests/R1.txt", response.get("Location"));
}
/**
* Test Redirection 3xx (Response Code)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-10.3">RFC 2616 (section 10.3)</a>
*/
@Test
public void test10_3_RedirectHttp11Resource() throws Exception
{
// HTTP/1.1 - redirect with resource/content
StringBuffer req4 = new StringBuffer();
req4.append("GET /redirect/R2.txt HTTP/1.1\n");
req4.append("Host: localhost\n");
req4.append("Connection: close\n");
req4.append("\n");
HttpTester.Response response = http.request(req4);
String specId = "10.3 Redirection HTTP/1.1 w/content";
assertThat(specId + " [status]",response.getStatus(),is(HttpStatus.FOUND_302));
assertThat(specId + " [location]",response.get("Location"),is(server.getScheme() + "://localhost/tests/R2.txt"));
assertThat(specId + " [connection]",response.get("Connection"),is("close"));
assertThat(specId + " [content-length]",response.get("Content-Length"), nullValue());
}
/**
* Test Accept-Encoding (Header Field)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.3">RFC 2616 (section 14.3)</a>
*/
@Test
public void test14_3_AcceptEncodingGzip() throws Exception
{
String specId;
// Gzip accepted
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/solutions.html HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Accept-Encoding: gzip\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
specId = "14.3 Accept-Encoding Header";
assertEquals(specId, HttpStatus.OK_200, response.getStatus());
assertEquals(specId,"gzip", response.get("Content-Encoding"));
assertEquals(specId,"text/html", response.get("Content-Type"));
}
/**
* Test Content-Range (Header Field)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.16">RFC 2616 (section 14.16)</a>
*/
@Test
public void test14_16_NoRange() throws Exception
{
//
// calibrate with normal request (no ranges); if this doesnt
// work, dont expect ranges to work either
//
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(response.getContent().contains(ALPHA));
}
private void assertPartialContentRange(String rangedef, String expectedRange, String expectedBody) throws IOException
{
// server should ignore all range headers which include
// at least one syntactically invalid range
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Range: ").append(rangedef).append("\n"); // Invalid range
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
String msg = "Partial Range: '" + rangedef + "'";
assertEquals(msg,HttpStatus.PARTIAL_CONTENT_206, response.getStatus());
assertEquals(msg,"bytes " + expectedRange, response.get("Content-Range"));
assertTrue(msg,response.getContent().contains(expectedBody));
}
/**
* Test Content-Range (Header Field) - Tests multiple ranges, where all defined ranges are syntactically valid, however some ranges are outside of the
* limits of the available data
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.16">RFC 2616 (section 14.16)</a>
*/
@Test
public void test14_16_PartialRange() throws Exception
{
String alpha = ALPHA;
// server should not return a 416 if at least one syntactically valid ranges
// are is satisfiable
assertPartialContentRange("bytes=5-8,50-60","5-8/27",alpha.substring(5,8 + 1));
assertPartialContentRange("bytes=50-60,5-8","5-8/27",alpha.substring(5,8 + 1));
}
/**
* Test Content-Range (Header Field) - Tests single Range request header with 2 ranges defined, where there is a mixed case of validity, 1 range invalid,
* another 1 valid.
*
* Only the valid range should be processed. The invalid range should be ignored.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.16">RFC 2616 (section 14.16)</a>
*/
@Test
public void test14_16_PartialRange_MixedRanges() throws Exception
{
String alpha = ALPHA;
// server should not return a 416 if at least one syntactically valid ranges
// are is satisfiable
//
// should test for combinations of good and syntactically
// invalid ranges here, but I am not certain what the right
// behavior is anymore
//
// return data for valid ranges while ignoring unsatisfiable
// ranges
// a) Range: bytes=a-b,5-8
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Range: bytes=a-b,5-8\n"); // Invalid range, then Valid range
req1.append("Connection: close\n");
req1.append("\n");
http.setTimeoutMillis(60000);
HttpTester.Response response = http.request(req1);
String msg = "Partial Range (Mixed): 'bytes=a-b,5-8'";
assertEquals(msg,HttpStatus.PARTIAL_CONTENT_206, response.getStatus());
assertEquals(msg,"bytes 5-8/27", response.get("Content-Range"));
assertTrue(msg,response.getContent().contains(alpha.substring(5,8 + 1)));
}
/**
* Test Content-Range (Header Field) - Tests single Range request header with 2 ranges defined, where there is a mixed case of validity, 1 range invalid,
* another 1 valid.
*
* Only the valid range should be processed. The invalid range should be ignored.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.16">RFC 2616 (section 14.16)</a>
*/
@Test
public void test14_16_PartialRange_MixedBytes() throws Exception
{
String alpha = ALPHA;
// server should not return a 416 if at least one syntactically valid ranges
// are is satisfiable
//
// should test for combinations of good and syntactically
// invalid ranges here, but I am not certain what the right
// behavior is anymore
//
// return data for valid ranges while ignoring unsatisfiable
// ranges
// b) Range: bytes=a-b,bytes=5-8
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Range: bytes=a-b,bytes=5-8\n"); // Invalid range, then Valid range
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
String msg = "Partial Range (Mixed): 'bytes=a-b,bytes=5-8'";
assertEquals(msg,HttpStatus.PARTIAL_CONTENT_206, response.getStatus());
assertEquals(msg,"bytes 5-8/27", response.get("Content-Range"));
assertTrue(msg,response.getContent().contains(alpha.substring(5,8 + 1)));
}
/**
* Test Content-Range (Header Field) - Tests multiple Range request headers, where there is a mixed case of validity, 1 range invalid, another 1 valid.
*
* Only the valid range should be processed. The invalid range should be ignored.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.16">RFC 2616 (section 14.16)</a>
*/
@Test
public void test14_16_PartialRange_MixedMultiple() throws Exception
{
String alpha = ALPHA;
// server should not return a 416 if at least one syntactically valid ranges
// are is satisfiable
//
// should test for combinations of good and syntactically
// invalid ranges here, but I am not certain what the right
// behavior is anymore
//
// return data for valid ranges while ignoring unsatisfiable
// ranges
// c) Range: bytes=a-b
// Range: bytes=5-8
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Range: bytes=a-b\n"); // Invalid range
req1.append("Range: bytes=5-8\n"); // Valid range
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
String msg = "Partial Range (Mixed): 'bytes=a-b' 'bytes=5-8'";
assertEquals(msg,HttpStatus.PARTIAL_CONTENT_206, response.getStatus());
assertEquals(msg,"bytes 5-8/27", response.get("Content-Range"));
assertTrue(msg,response.getContent().contains(alpha.substring(5,8 + 1)));
}
/**
* Test Host (Header Field)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.23">RFC 2616 (section 14.23)</a>
*/
@Test
public void test14_23_Http10_NoHostHeader() throws Exception
{
// HTTP/1.0 OK with no host
StringBuffer req1 = new StringBuffer();
req1.append("GET /tests/R1.txt HTTP/1.0\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
assertEquals("14.23 HTTP/1.0 - No Host", HttpStatus.OK_200, response.getStatus());
}
/**
* Test Host (Header Field)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.23">RFC 2616 (section 14.23)</a>
*/
@Test
public void test14_23_Http11_NoHost() throws Exception
{
// HTTP/1.1 400 (bad request) with no host
StringBuffer req2 = new StringBuffer();
req2.append("GET /tests/R1.txt HTTP/1.1\n");
req2.append("Connection: close\n");
req2.append("\n");
HttpTester.Response response = http.request(req2);
assertEquals("14.23 HTTP/1.1 - No Host",HttpStatus.BAD_REQUEST_400, response.getStatus());
}
/**
* Test Host (Header Field)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.23">RFC 2616 (section 14.23)</a>
*/
@Test
public void test14_23_ValidHost() throws Exception
{
// HTTP/1.1 - Valid host
StringBuffer req3 = new StringBuffer();
req3.append("GET /tests/R1.txt HTTP/1.1\n");
req3.append("Host: localhost\n");
req3.append("Connection: close\n");
req3.append("\n");
HttpTester.Response response = http.request(req3);
assertEquals("14.23 HTTP/1.1 - Valid Host", HttpStatus.OK_200, response.getStatus());
}
/**
* Test Host (Header Field)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.23">RFC 2616 (section 14.23)</a>
*/
@Test
public void test14_23_IncompleteHostHeader() throws Exception
{
// HTTP/1.1 - Incomplete (empty) Host header
try (StacklessLogging stackless = new StacklessLogging(HttpParser.class))
{
StringBuffer req4 = new StringBuffer();
req4.append("GET /tests/R1.txt HTTP/1.1\n");
req4.append("Host:\n");
req4.append("Connection: close\n");
req4.append("\n");
HttpTester.Response response = http.request(req4);
assertEquals("14.23 HTTP/1.1 - Empty Host", HttpStatus.OK_200, response.getStatus());
}
}
/**
* Tests the (byte) "Range" header for partial content.
*
* Note: This is similar to {@link #assertPartialContentRange(String, String, String)} but uses the "Range" header and not the "Content-Range" header.
*
* @param rangedef
* @param expectedRange
* @param expectedBody
* @throws IOException
*/
private void assertByteRange(String rangedef, String expectedRange, String expectedBody) throws IOException
{
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Range: ").append(rangedef).append("\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
String msg = "Partial (Byte) Range: '" + rangedef + "'";
assertEquals(msg,HttpStatus.PARTIAL_CONTENT_206, response.getStatus());
// It might be strange to see a "Content-Range' response header to a 'Range' request,
// but this is appropriate per the RFC2616 spec.
assertEquals(msg,"bytes " + expectedRange, response.get("Content-Range"));
assertTrue(msg,response.getContent().contains(expectedBody));
}
/**
* Test Range (Header Field)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.35">RFC 2616 (section 14.35)</a>
*/
@Test
public void test14_35_Range() throws Exception
{
//
// test various valid range specs that have not been
// tested yet
//
String alpha = ALPHA;
// First 3 bytes
assertByteRange("bytes=0-2","0-2/27",alpha.substring(0,2 + 1));
// From byte offset 23 thru the end of the content
assertByteRange("bytes=23-","23-26/27",alpha.substring(23));
// Request byte offset 23 thru 42 (only 26 bytes in content)
// The last 3 bytes are returned.
assertByteRange("bytes=23-42","23-26/27",alpha.substring(23,26 + 1));
// Request the last 3 bytes
assertByteRange("bytes=-3","24-26/27",alpha.substring(24,26 + 1));
}
/**
* Test Range (Header Field)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.35">RFC 2616 (section 14.35)</a>
*/
@Test
public void test14_35_Range_Multipart1() throws Exception
{
String rangedef = "23-23,-2"; // Request byte at offset 23, and the last 2 bytes
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Range: ").append(rangedef).append("\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
String msg = "Partial (Byte) Range: '" + rangedef + "'";
assertEquals(msg,HttpStatus.PARTIAL_CONTENT_206, response.getStatus());
String contentType = response.get("Content-Type");
// RFC states that multiple parts should result in multipart/byteranges Content type.
StringAssert.assertContains(msg + " Content-Type",contentType,"multipart/byteranges");
// Collect 'boundary' string
String boundary = null;
String parts[] = StringUtil.split(contentType,';');
for (int i = 0; i < parts.length; i++)
{
if (parts[i].trim().startsWith("boundary="))
{
String boundparts[] = StringUtil.split(parts[i],'=');
Assert.assertEquals(msg + " Boundary parts.length",2,boundparts.length);
boundary = boundparts[1];
}
}
Assert.assertNotNull(msg + " Should have found boundary in Content-Type header",boundary);
List<String> lines = StringUtil.asLines(response.getContent().trim());
int i=0;
assertEquals("--"+boundary,lines.get(i++));
assertEquals("Content-Type: text/plain",lines.get(i++));
assertEquals("Content-Range: bytes 23-23/27",lines.get(i++));
assertEquals("",lines.get(i++));
assertEquals("X",lines.get(i++));
assertEquals("--"+boundary,lines.get(i++));
assertEquals("Content-Type: text/plain",lines.get(i++));
assertEquals("Content-Range: bytes 25-26/27",lines.get(i++));
assertEquals("",lines.get(i++));
assertEquals("Z",lines.get(i++));
assertEquals("",lines.get(i++));
assertEquals("--"+boundary+"--",lines.get(i++));
}
/**
* Test Range (Header Field)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.35">RFC 2616 (section 14.35)</a>
*/
@Test
public void test14_35_Range_Multipart2() throws Exception
{
// Request the last 1 byte, last 2 bytes, and last 3 bytes.
// This is an example of overlapping ranges
String rangedef = "-1,-2,-3";
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Range: ").append(rangedef).append("\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
// System.err.println(response+response.getContent());
String msg = "Partial (Byte) Range: '" + rangedef + "'";
assertEquals(msg,HttpStatus.PARTIAL_CONTENT_206,response.getStatus());
String contentType = response.get("Content-Type");
// RFC states that multiple parts should result in multipart/byteranges Content type.
StringAssert.assertContains(msg + " Content-Type",contentType,"multipart/byteranges");
// Collect 'boundary' string
String boundary = null;
String parts[] = StringUtil.split(contentType,';');
for (int i = 0; i < parts.length; i++)
{
if (parts[i].trim().startsWith("boundary="))
{
String boundparts[] = StringUtil.split(parts[i],'=');
Assert.assertEquals(msg + " Boundary parts.length",2,boundparts.length);
boundary = boundparts[1];
}
}
Assert.assertNotNull(msg + " Should have found boundary in Content-Type header",boundary);
List<String> lines = StringUtil.asLines(response.getContent().trim());
int i=0;
assertEquals("--"+boundary,lines.get(i++));
assertEquals("Content-Type: text/plain",lines.get(i++));
assertEquals("Content-Range: bytes 26-26/27",lines.get(i++));
assertEquals("",lines.get(i++));
assertEquals("",lines.get(i++));
assertEquals("",lines.get(i++));
assertEquals("--"+boundary,lines.get(i++));
assertEquals("Content-Type: text/plain",lines.get(i++));
assertEquals("Content-Range: bytes 25-26/27",lines.get(i++));
assertEquals("",lines.get(i++));
assertEquals("Z",lines.get(i++));
assertEquals("",lines.get(i++));
assertEquals("--"+boundary,lines.get(i++));
assertEquals("Content-Type: text/plain",lines.get(i++));
assertEquals("Content-Range: bytes 24-26/27",lines.get(i++));
assertEquals("",lines.get(i++));
assertEquals("YZ",lines.get(i++));
assertEquals("",lines.get(i++));
assertEquals("--"+boundary+"--",lines.get(i++));
}
/**
* Test Range (Header Field)
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.35">RFC 2616 (section 14.35)</a>
*/
@Test
public void test14_35_PartialRange() throws Exception
{
//
// test various valid range specs that have not been
// tested yet
//
String alpha = ALPHA;
// server should not return a 416 if at least one syntactically valid ranges
// are is satisfiable
assertByteRange("bytes=5-8,50-60","5-8/27",alpha.substring(5,8 + 1));
assertByteRange("bytes=50-60,5-8","5-8/27",alpha.substring(5,8 + 1));
}
private void assertBadByteRange(String rangedef) throws IOException
{
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/alpha.txt HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("Range: ").append(rangedef).append("\n"); // Invalid range
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
assertEquals("BadByteRange: '" + rangedef + "'",HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE_416, response.getStatus());
}
/**
* Test Range (Header Field) - Bad Range Request
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.35">RFC 2616 (section 14.35)</a>
*/
@Test
public void test14_35_BadRange_InvalidSyntax() throws Exception
{
// server should ignore all range headers which include
// at least one syntactically invalid range
assertBadByteRange("bytes=a-b"); // Invalid due to non-digit entries
assertBadByteRange("bytes=-"); // Invalid due to missing range ends
assertBadByteRange("bytes=-1-"); // Invalid due negative to end range
assertBadByteRange("doublehalfwords=1-2"); // Invalid due to bad key 'doublehalfwords'
}
/**
* Test TE (Header Field) / Transfer Codings
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.39">RFC 2616 (section 14.39)</a>
*/
@Test
public void test14_39_TEGzip() throws Exception
{
if (STRICT)
{
String specId;
// Gzip accepted
StringBuffer req1 = new StringBuffer();
req1.append("GET /rfc2616-webapp/solutions.html HTTP/1.1\n");
req1.append("Host: localhost\n");
req1.append("TE: gzip\n");
req1.append("Connection: close\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
specId = "14.39 TE Header";
assertEquals(specId, HttpStatus.OK_200, response.getStatus());
assertEquals(specId,"gzip", response.get("Transfer-Encoding"));
}
}
/**
* Test TE (Header Field) / Transfer Codings
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.39">RFC 2616 (section 14.39)</a>
*/
@Test
public void test14_39_TEDeflate() throws Exception
{
if (STRICT)
{
String specId;
// Deflate not accepted
StringBuffer req2 = new StringBuffer();
req2.append("GET /rfc2616-webapp/solutions.html HTTP/1.1\n");
req2.append("Host: localhost\n");
req2.append("TE: deflate\n"); // deflate not accepted
req2.append("Connection: close\n");
req2.append("\n");
HttpTester.Response response = http.request(req2);
specId = "14.39 TE Header";
assertEquals(specId,HttpStatus.NOT_IMPLEMENTED_501, response.getStatus()); // Error on TE (deflate not supported)
}
}
/**
* Test Compatibility with Previous (HTTP) Versions.
*
* @see <a href="http://tools.ietf.org/html/rfc2616#section-19.6">RFC 2616 (section 19.6)</a>
*/
@Test
public void test19_6() throws Exception
{
String specId;
/* Compatibility with HTTP/1.0 */
StringBuffer req1 = new StringBuffer();
req1.append("GET /tests/R1.txt HTTP/1.0\n");
req1.append("\n");
HttpTester.Response response = http.request(req1);
specId = "19.6 Compatibility with HTTP/1.0 - simple request";
assertEquals(specId, HttpStatus.OK_200, response.getStatus());
assertTrue(specId + " - connection closed not assumed",response.get("Connection") == null);
/* Compatibility with HTTP/1.0 */
StringBuffer req2 = new StringBuffer();
req2.append("GET /tests/R1.txt HTTP/1.0\n");
req2.append("Host: localhost\n");
req2.append("Connection: keep-alive\n");
req2.append("\n");
req2.append("GET /tests/R2.txt HTTP/1.0\n");
req2.append("Host: localhost\n");
req2.append("Connection: close\n"); // Connection closed here
req2.append("\n");
req2.append("GET /tests/R3.txt HTTP/1.0\n"); // This request should not be handled
req2.append("Host: localhost\n");
req2.append("Connection: close\n");
req2.append("\n");
List<HttpTester.Response> responses = http.requests(req2);
// Since R2 closes the connection, should only get 2 responses (R1 &
// R2), not (R3)
Assert.assertEquals("Response Count",2,responses.size());
response = responses.get(0); // response 1
specId = "19.6.2 Compatibility with previous HTTP - Keep-alive";
assertEquals(specId, HttpStatus.OK_200, response.getStatus());
assertEquals(specId,"keep-alive", response.get("Connection"));
assertTrue(specId,response.getContent().contains("Resource=R1"));
response = responses.get(1); // response 2
assertEquals(specId, HttpStatus.OK_200, response.getStatus());
assertTrue(specId,response.getContent().contains("Resource=R2"));
/* Compatibility with HTTP/1.0 */
StringBuffer req3 = new StringBuffer();
req3.append("GET /echo/R1 HTTP/1.0\n");
req3.append("Host: localhost\n");
req3.append("Connection: keep-alive\n");
req3.append("Content-Length: 10\n");
req3.append("\n");
req3.append("1234567890\n");
req3.append("GET /echo/RA HTTP/1.0\n");
req3.append("Host: localhost\n");
req3.append("Connection: keep-alive\n");
req3.append("Content-Length: 10\n");
req3.append("\n");
req3.append("ABCDEFGHIJ\n");
req3.append("GET /tests/R2.txt HTTP/1.0\n");
req3.append("Host: localhost\n");
req3.append("Connection: close\n"); // Close connection here
req3.append("\n");
req3.append("GET /tests/R3.txt HTTP/1.0\n"); // This request should not
// be handled.
req3.append("Host: localhost\n");
req3.append("Connection: close\n");
req3.append("\n");
responses = http.requests(req3);
Assert.assertEquals("Response Count",3,responses.size());
specId = "19.6.2 Compatibility with HTTP/1.0- Keep-alive";
response = responses.get(0);
assertEquals(specId, HttpStatus.OK_200, response.getStatus());
assertEquals(specId,"keep-alive", response.get("Connection"));
assertTrue(specId, response.getContent().contains("1234567890\n"));
response = responses.get(1);
assertEquals(specId, HttpStatus.OK_200, response.getStatus());
assertEquals(specId, "keep-alive", response.get("Connection"));
assertTrue(specId,response.getContent().contains("ABCDEFGHIJ\n"));
response = responses.get(2);
assertEquals(specId, HttpStatus.OK_200, response.getStatus());
assertTrue(specId,response.getContent().contains("Host=Default\nResource=R2\n"));
}
protected void assertDate(String msg, Calendar expectedTime, long actualTime)
{
SimpleDateFormat sdf = new SimpleDateFormat("EEEE, d MMMM yyyy HH:mm:ss:SSS zzz");
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
String actual = sdf.format(new Date(actualTime));
String expected = sdf.format(expectedTime.getTime());
Assert.assertEquals(msg,expected,actual);
}
}