// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "content/child/multipart_response_delegate.h" #include #include "testing/gtest/include/gtest/gtest.h" #include "third_party/WebKit/public/platform/WebString.h" #include "third_party/WebKit/public/platform/WebURL.h" #include "third_party/WebKit/public/platform/WebURLLoaderClient.h" #include "third_party/WebKit/public/platform/WebURLResponse.h" using blink::WebString; using blink::WebURL; using blink::WebURLError; using blink::WebURLLoader; using blink::WebURLLoaderClient; using blink::WebURLRequest; using blink::WebURLResponse; using std::string; namespace content { class MultipartResponseDelegateTester { public: MultipartResponseDelegateTester(MultipartResponseDelegate* delegate) : delegate_(delegate) { } int PushOverLine(const std::string& data, size_t pos) { return delegate_->PushOverLine(data, pos); } bool ParseHeaders() { return delegate_->ParseHeaders(); } size_t FindBoundary() { return delegate_->FindBoundary(); } std::string& boundary() { return delegate_->boundary_; } std::string& data() { return delegate_->data_; } private: MultipartResponseDelegate* delegate_; }; namespace { class MultipartResponseTest : public testing::Test { }; class MockWebURLLoaderClient : public WebURLLoaderClient { public: MockWebURLLoaderClient() { Reset(); } void willFollowRedirect(WebURLLoader*, WebURLRequest&, const WebURLResponse&) override {} void didSendData(WebURLLoader*, unsigned long long, unsigned long long) override {} void didReceiveResponse(WebURLLoader* loader, const WebURLResponse& response) override { ++received_response_; response_ = response; data_.clear(); } void didReceiveData(blink::WebURLLoader* loader, const char* data, int data_length, int encoded_data_length) override { ++received_data_; data_.append(data, data_length); total_encoded_data_length_ += encoded_data_length; } void didFinishLoading(WebURLLoader*, double finishTime, int64_t total_encoded_data_length) override {} void didFail(WebURLLoader*, const WebURLError&) override {} void Reset() { received_response_ = received_data_ = total_encoded_data_length_ = 0; data_.clear(); response_.reset(); } string GetResponseHeader(const char* name) const { return string(response_.httpHeaderField(WebString::fromUTF8(name)).utf8()); } int received_response_, received_data_, total_encoded_data_length_; string data_; WebURLResponse response_; }; // We can't put this in an anonymous function because it's a friend class for // access to private members. TEST(MultipartResponseTest, Functions) { // PushOverLine tests WebURLResponse response; response.initialize(); response.setMIMEType("multipart/x-mixed-replace"); response.setHTTPHeaderField("Foo", "Bar"); response.setHTTPHeaderField("Content-type", "text/plain"); MockWebURLLoaderClient client; MultipartResponseDelegate delegate(&client, NULL, response, "bound"); MultipartResponseDelegateTester delegate_tester(&delegate); struct { const char* input; const int position; const int expected; } line_tests[] = { { "Line", 0, 0 }, { "Line", 2, 0 }, { "Line", 10, 0 }, { "\r\nLine", 0, 2 }, { "\nLine", 0, 1 }, { "\n\nLine", 0, 2 }, { "\rLine", 0, 1 }, { "Line\r\nLine", 4, 2 }, { "Line\nLine", 4, 1 }, { "Line\n\nLine", 4, 2 }, { "Line\rLine", 4, 1 }, { "Line\r\rLine", 4, 1 }, }; for (size_t i = 0; i < arraysize(line_tests); ++i) { EXPECT_EQ(line_tests[i].expected, delegate_tester.PushOverLine(line_tests[i].input, line_tests[i].position)); } // ParseHeaders tests struct { const char* data; const bool rv; const int received_response_calls; const char* newdata; } header_tests[] = { { "This is junk", false, 0, "This is junk" }, { "Foo: bar\nBaz:\n\nAfter:\n", true, 1, "After:\n" }, { "Foo: bar\nBaz:\n", false, 0, "Foo: bar\nBaz:\n" }, { "Foo: bar\r\nBaz:\r\n\r\nAfter:\r\n", true, 1, "After:\r\n" }, { "Foo: bar\r\nBaz:\r\n", false, 0, "Foo: bar\r\nBaz:\r\n" }, { "Foo: bar\nBaz:\r\n\r\nAfter:\n\n", true, 1, "After:\n\n" }, { "Foo: bar\r\nBaz:\n", false, 0, "Foo: bar\r\nBaz:\n" }, { "\r\n", true, 1, "" }, }; for (size_t i = 0; i < arraysize(header_tests); ++i) { client.Reset(); delegate_tester.data().assign(header_tests[i].data); EXPECT_EQ(header_tests[i].rv, delegate_tester.ParseHeaders()); EXPECT_EQ(header_tests[i].received_response_calls, client.received_response_); EXPECT_EQ(string(header_tests[i].newdata), delegate_tester.data()); } // Test that the resource response is filled in correctly when parsing // headers. client.Reset(); string test_header("content-type: image/png\ncontent-length: 10\n\n"); delegate_tester.data().assign(test_header); EXPECT_TRUE(delegate_tester.ParseHeaders()); EXPECT_TRUE(delegate_tester.data().length() == 0); EXPECT_EQ(string("image/png"), client.GetResponseHeader("Content-Type")); EXPECT_EQ(string("10"), client.GetResponseHeader("content-length")); // This header is passed from the original request. EXPECT_EQ(string("Bar"), client.GetResponseHeader("foo")); // Make sure we parse the right mime-type if a charset is provided. client.Reset(); string test_header2("content-type: text/html; charset=utf-8\n\n"); delegate_tester.data().assign(test_header2); EXPECT_TRUE(delegate_tester.ParseHeaders()); EXPECT_TRUE(delegate_tester.data().length() == 0); EXPECT_EQ(string("text/html; charset=utf-8"), client.GetResponseHeader("Content-Type")); EXPECT_EQ(string("utf-8"), string(client.response_.textEncodingName().utf8())); // FindBoundary tests struct { const char* boundary; const char* data; const size_t position; } boundary_tests[] = { { "bound", "bound", 0 }, { "bound", "--bound", 0 }, { "bound", "junkbound", 4 }, { "bound", "junk--bound", 4 }, { "foo", "bound", string::npos }, { "bound", "--boundbound", 0 }, }; for (size_t i = 0; i < arraysize(boundary_tests); ++i) { delegate_tester.boundary().assign(boundary_tests[i].boundary); delegate_tester.data().assign(boundary_tests[i].data); EXPECT_EQ(boundary_tests[i].position, delegate_tester.FindBoundary()); } } TEST(MultipartResponseTest, MissingBoundaries) { WebURLResponse response; response.initialize(); response.setMIMEType("multipart/x-mixed-replace"); response.setHTTPHeaderField("Foo", "Bar"); response.setHTTPHeaderField("Content-type", "text/plain"); MockWebURLLoaderClient client; MultipartResponseDelegate delegate(&client, NULL, response, "bound"); // No start boundary string no_start_boundary( "Content-type: text/plain\n\n" "This is a sample response\n" "--bound--" "ignore junk after end token --bound\n\nTest2\n"); delegate.OnReceivedData(no_start_boundary.c_str(), static_cast(no_start_boundary.length()), static_cast(no_start_boundary.length())); EXPECT_EQ(1, client.received_response_); EXPECT_EQ(1, client.received_data_); EXPECT_EQ(string("This is a sample response"), client.data_); EXPECT_EQ(static_cast(no_start_boundary.length()), client.total_encoded_data_length_); delegate.OnCompletedRequest(); EXPECT_EQ(1, client.received_response_); EXPECT_EQ(1, client.received_data_); // No end boundary client.Reset(); MultipartResponseDelegate delegate2(&client, NULL, response, "bound"); string no_end_boundary( "bound\nContent-type: text/plain\n\n" "This is a sample response\n"); delegate2.OnReceivedData(no_end_boundary.c_str(), static_cast(no_end_boundary.length()), static_cast(no_end_boundary.length())); EXPECT_EQ(1, client.received_response_); EXPECT_EQ(1, client.received_data_); EXPECT_EQ("This is a sample response\n", client.data_); EXPECT_EQ(static_cast(no_end_boundary.length()), client.total_encoded_data_length_); delegate2.OnCompletedRequest(); EXPECT_EQ(1, client.received_response_); EXPECT_EQ(1, client.received_data_); EXPECT_EQ(string("This is a sample response\n"), client.data_); EXPECT_EQ(static_cast(no_end_boundary.length()), client.total_encoded_data_length_); // Neither boundary client.Reset(); MultipartResponseDelegate delegate3(&client, NULL, response, "bound"); string no_boundaries( "Content-type: text/plain\n\n" "This is a sample response\n"); delegate3.OnReceivedData(no_boundaries.c_str(), static_cast(no_boundaries.length()), static_cast(no_boundaries.length())); EXPECT_EQ(1, client.received_response_); EXPECT_EQ(1, client.received_data_); EXPECT_EQ("This is a sample response\n", client.data_); EXPECT_EQ(static_cast(no_boundaries.length()), client.total_encoded_data_length_); delegate3.OnCompletedRequest(); EXPECT_EQ(1, client.received_response_); EXPECT_EQ(1, client.received_data_); EXPECT_EQ(string("This is a sample response\n"), client.data_); EXPECT_EQ(static_cast(no_boundaries.length()), client.total_encoded_data_length_); } TEST(MultipartResponseTest, MalformedBoundary) { // Some servers send a boundary that is prefixed by "--". See bug 5786. WebURLResponse response; response.initialize(); response.setMIMEType("multipart/x-mixed-replace"); response.setHTTPHeaderField("Foo", "Bar"); response.setHTTPHeaderField("Content-type", "text/plain"); MockWebURLLoaderClient client; MultipartResponseDelegate delegate(&client, NULL, response, "--bound"); string data( "--bound\n" "Content-type: text/plain\n\n" "This is a sample response\n" "--bound--" "ignore junk after end token --bound\n\nTest2\n"); delegate.OnReceivedData(data.c_str(), static_cast(data.length()), static_cast(data.length())); EXPECT_EQ(1, client.received_response_); EXPECT_EQ(1, client.received_data_); EXPECT_EQ(string("This is a sample response"), client.data_); EXPECT_EQ(static_cast(data.length()), client.total_encoded_data_length_); delegate.OnCompletedRequest(); EXPECT_EQ(1, client.received_response_); EXPECT_EQ(1, client.received_data_); } // Used in for tests that break the data in various places. struct TestChunk { const int start_pos; // offset in data const int end_pos; // end offset in data const int expected_responses; const int expected_received_data; const char* expected_data; const int expected_encoded_data_length; }; void VariousChunkSizesTest(const TestChunk chunks[], int chunks_size, int responses, int received_data, const char* completed_data, int completed_encoded_data_length) { const string data( "--bound\n" // 0-7 "Content-type: image/png\n\n" // 8-32 "datadatadatadatadata" // 33-52 "--bound\n" // 53-60 "Content-type: image/jpg\n\n" // 61-85 "foofoofoofoofoo" // 86-100 "--bound--"); // 101-109 WebURLResponse response; response.initialize(); response.setMIMEType("multipart/x-mixed-replace"); MockWebURLLoaderClient client; MultipartResponseDelegate delegate(&client, NULL, response, "bound"); for (int i = 0; i < chunks_size; ++i) { ASSERT_TRUE(chunks[i].start_pos < chunks[i].end_pos); string chunk = data.substr(chunks[i].start_pos, chunks[i].end_pos - chunks[i].start_pos); delegate.OnReceivedData( chunk.c_str(), static_cast(chunk.length()), static_cast(chunk.length())); EXPECT_EQ(chunks[i].expected_responses, client.received_response_); EXPECT_EQ(chunks[i].expected_received_data, client.received_data_); EXPECT_EQ(string(chunks[i].expected_data), client.data_); EXPECT_EQ(chunks[i].expected_encoded_data_length, client.total_encoded_data_length_); } // Check final state delegate.OnCompletedRequest(); EXPECT_EQ(responses, client.received_response_); EXPECT_EQ(received_data, client.received_data_); string completed_data_string(completed_data); EXPECT_EQ(completed_data_string, client.data_); EXPECT_EQ(completed_encoded_data_length, client.total_encoded_data_length_); } TEST(MultipartResponseTest, BreakInBoundary) { // Break in the first boundary const TestChunk bound1[] = { { 0, 4, 0, 0, "", 0 }, { 4, 110, 2, 2, "foofoofoofoofoo", 110 }, }; VariousChunkSizesTest(bound1, arraysize(bound1), 2, 2, "foofoofoofoofoo", 110); // Break in first and second const TestChunk bound2[] = { { 0, 4, 0, 0, "", 0 }, { 4, 55, 1, 1, "datadatadatadat", 55 }, { 55, 65, 1, 2, "datadatadatadatadata", 65 }, { 65, 110, 2, 3, "foofoofoofoofoo", 110 }, }; VariousChunkSizesTest(bound2, arraysize(bound2), 2, 3, "foofoofoofoofoo", 110); // Break in second only const TestChunk bound3[] = { { 0, 55, 1, 1, "datadatadatadat", 55 }, { 55, 110, 2, 3, "foofoofoofoofoo", 110 }, }; VariousChunkSizesTest(bound3, arraysize(bound3), 2, 3, "foofoofoofoofoo", 110); } TEST(MultipartResponseTest, BreakInHeaders) { // Break in first header const TestChunk header1[] = { { 0, 10, 0, 0, "", 0 }, { 10, 35, 1, 0, "", 0 }, { 35, 110, 2, 2, "foofoofoofoofoo", 110 }, }; VariousChunkSizesTest(header1, arraysize(header1), 2, 2, "foofoofoofoofoo", 110); // Break in both headers const TestChunk header2[] = { { 0, 10, 0, 0, "", 0 }, { 10, 65, 1, 1, "datadatadatadatadata", 65 }, { 65, 110, 2, 2, "foofoofoofoofoo", 110 }, }; VariousChunkSizesTest(header2, arraysize(header2), 2, 2, "foofoofoofoofoo", 110); // Break at end of a header const TestChunk header3[] = { { 0, 33, 1, 0, "", 0 }, { 33, 65, 1, 1, "datadatadatadatadata", 65 }, { 65, 110, 2, 2, "foofoofoofoofoo", 110 }, }; VariousChunkSizesTest(header3, arraysize(header3), 2, 2, "foofoofoofoofoo", 110); } TEST(MultipartResponseTest, BreakInData) { // All data as one chunk const TestChunk data1[] = { { 0, 110, 2, 2, "foofoofoofoofoo", 110 }, }; VariousChunkSizesTest(data1, arraysize(data1), 2, 2, "foofoofoofoofoo", 110); // breaks in data segment const TestChunk data2[] = { { 0, 35, 1, 0, "", 0 }, { 35, 65, 1, 1, "datadatadatadatadata", 65 }, { 65, 90, 2, 1, "", 65 }, { 90, 110, 2, 2, "foofoofoofoofoo", 110 }, }; VariousChunkSizesTest(data2, arraysize(data2), 2, 2, "foofoofoofoofoo", 110); // Incomplete send const TestChunk data3[] = { { 0, 35, 1, 0, "", 0 }, { 35, 90, 2, 1, "", 90 }, }; VariousChunkSizesTest(data3, arraysize(data3), 2, 2, "foof", 90); } TEST(MultipartResponseTest, SmallChunk) { WebURLResponse response; response.initialize(); response.setMIMEType("multipart/x-mixed-replace"); response.setHTTPHeaderField("Content-type", "text/plain"); MockWebURLLoaderClient client; MultipartResponseDelegate delegate(&client, NULL, response, "bound"); // Test chunks of size 1, 2, and 0. string data( "--boundContent-type: text/plain\n\n" "\n--boundContent-type: text/plain\n\n" "\n\n--boundContent-type: text/plain\n\n" "--boundContent-type: text/plain\n\n" "end--bound--"); delegate.OnReceivedData(data.c_str(), static_cast(data.length()), static_cast(data.length())); EXPECT_EQ(4, client.received_response_); EXPECT_EQ(2, client.received_data_); EXPECT_EQ(string("end"), client.data_); EXPECT_EQ(static_cast(data.length()), client.total_encoded_data_length_); delegate.OnCompletedRequest(); EXPECT_EQ(4, client.received_response_); EXPECT_EQ(2, client.received_data_); } TEST(MultipartResponseTest, MultipleBoundaries) { // Test multiple boundaries back to back WebURLResponse response; response.initialize(); response.setMIMEType("multipart/x-mixed-replace"); MockWebURLLoaderClient client; MultipartResponseDelegate delegate(&client, NULL, response, "bound"); string data("--bound\r\n\r\n--bound\r\n\r\nfoofoo--bound--"); delegate.OnReceivedData(data.c_str(), static_cast(data.length()), static_cast(data.length())); EXPECT_EQ(2, client.received_response_); EXPECT_EQ(1, client.received_data_); EXPECT_EQ(string("foofoo"), client.data_); EXPECT_EQ(static_cast(data.length()), client.total_encoded_data_length_); } TEST(MultipartResponseTest, MultipartByteRangeParsingTest) { // Test multipart/byteranges based boundary parsing. WebURLResponse response1; response1.initialize(); response1.setMIMEType("multipart/x-mixed-replace"); response1.setHTTPHeaderField("Content-Length", "200"); response1.setHTTPHeaderField("Content-type", "multipart/byteranges; boundary=--bound--"); std::string multipart_boundary; bool result = MultipartResponseDelegate::ReadMultipartBoundary( response1, &multipart_boundary); EXPECT_EQ(result, true); EXPECT_EQ(string("--bound--"), multipart_boundary); WebURLResponse response2; response2.initialize(); response2.setMIMEType("image/png"); response2.setHTTPHeaderField("Content-Length", "300"); response2.setHTTPHeaderField("Last-Modified", "Mon, 04 Apr 2005 20:36:01 GMT"); response2.setHTTPHeaderField("Date", "Thu, 11 Sep 2008 18:21:42 GMT"); multipart_boundary.clear(); result = MultipartResponseDelegate::ReadMultipartBoundary( response2, &multipart_boundary); EXPECT_EQ(result, false); WebURLResponse response3; response3.initialize(); response3.setMIMEType("multipart/byteranges"); response3.setHTTPHeaderField("Content-Length", "300"); response3.setHTTPHeaderField("Last-Modified", "Mon, 04 Apr 2005 20:36:01 GMT"); response3.setHTTPHeaderField("Date", "Thu, 11 Sep 2008 18:21:42 GMT"); response3.setHTTPHeaderField("Content-type", "multipart/byteranges"); multipart_boundary.clear(); result = MultipartResponseDelegate::ReadMultipartBoundary( response3, &multipart_boundary); EXPECT_EQ(result, false); EXPECT_EQ(multipart_boundary.length(), 0U); WebURLResponse response4; response4.initialize(); response4.setMIMEType("multipart/byteranges"); response4.setHTTPHeaderField("Content-Length", "200"); response4.setHTTPHeaderField("Content-type", "multipart/byteranges; boundary=--bound--; charSet=utf8"); multipart_boundary.clear(); result = MultipartResponseDelegate::ReadMultipartBoundary( response4, &multipart_boundary); EXPECT_EQ(result, true); EXPECT_EQ(string("--bound--"), multipart_boundary); WebURLResponse response5; response5.initialize(); response5.setMIMEType("multipart/byteranges"); response5.setHTTPHeaderField("Content-Length", "200"); response5.setHTTPHeaderField("Content-type", "multipart/byteranges; boundary=\"--bound--\"; charSet=utf8"); multipart_boundary.clear(); result = MultipartResponseDelegate::ReadMultipartBoundary( response5, &multipart_boundary); EXPECT_EQ(result, true); EXPECT_EQ(string("--bound--"), multipart_boundary); } TEST(MultipartResponseTest, MultipartContentRangesTest) { WebURLResponse response1; response1.initialize(); response1.setMIMEType("application/pdf"); response1.setHTTPHeaderField("Content-Length", "200"); // Ignored! // Use intentionally >32bit values to check they are handled correctly. response1.setHTTPHeaderField("Content-Range", "bytes 5000000000-5000000050/6000000000"); int64 content_range_lower_bound = 0; int64 content_range_upper_bound = 0; int64 content_range_instance_size = 0; bool result = MultipartResponseDelegate::ReadContentRanges( response1, &content_range_lower_bound, &content_range_upper_bound, &content_range_instance_size); EXPECT_EQ(result, true); EXPECT_EQ(content_range_lower_bound, 5e9); EXPECT_EQ(content_range_upper_bound, 5e9+50); EXPECT_EQ(content_range_instance_size, 6e9); WebURLResponse response2; response2.initialize(); response2.setMIMEType("application/pdf"); response2.setHTTPHeaderField("Content-Length", "200"); response2.setHTTPHeaderField("Content-Range", "bytes 1000/1050"); content_range_lower_bound = 0; content_range_upper_bound = 0; content_range_instance_size = 0; result = MultipartResponseDelegate::ReadContentRanges( response2, &content_range_lower_bound, &content_range_upper_bound, &content_range_instance_size); EXPECT_EQ(result, false); WebURLResponse response3; response3.initialize(); response3.setMIMEType("application/pdf"); response3.setHTTPHeaderField("Content-Length", "200"); response3.setHTTPHeaderField("Range", "bytes 1000-1050/5000"); content_range_lower_bound = 0; content_range_upper_bound = 0; content_range_instance_size = 0; result = MultipartResponseDelegate::ReadContentRanges( response3, &content_range_lower_bound, &content_range_upper_bound, &content_range_instance_size); EXPECT_EQ(result, true); EXPECT_EQ(content_range_lower_bound, 1000); EXPECT_EQ(content_range_upper_bound, 1050); WebURLResponse response4; response4.initialize(); response4.setMIMEType("application/pdf"); response4.setHTTPHeaderField("Content-Length", "200"); content_range_lower_bound = 0; content_range_upper_bound = 0; content_range_instance_size = 0; result = MultipartResponseDelegate::ReadContentRanges( response4, &content_range_lower_bound, &content_range_upper_bound, &content_range_instance_size); EXPECT_EQ(result, false); } TEST(MultipartResponseTest, MultipartPayloadSet) { WebURLResponse response; response.initialize(); response.setMIMEType("multipart/x-mixed-replace"); MockWebURLLoaderClient client; MultipartResponseDelegate delegate(&client, NULL, response, "bound"); string data( "--bound\n" "Content-type: text/plain\n\n" "response data\n" "--bound\n"); delegate.OnReceivedData(data.c_str(), static_cast(data.length()), static_cast(data.length())); EXPECT_EQ(1, client.received_response_); EXPECT_EQ(string("response data"), client.data_); EXPECT_EQ(static_cast(data.length()), client.total_encoded_data_length_); EXPECT_FALSE(client.response_.isMultipartPayload()); string data2( "Content-type: text/plain\n\n" "response data2\n" "--bound\n"); delegate.OnReceivedData(data2.c_str(), static_cast(data2.length()), static_cast(data2.length())); EXPECT_EQ(2, client.received_response_); EXPECT_EQ(string("response data2"), client.data_); EXPECT_EQ(static_cast(data.length()) + static_cast(data2.length()), client.total_encoded_data_length_); EXPECT_TRUE(client.response_.isMultipartPayload()); } } // namespace } // namespace content