From 79aba5a54291da8765270317b5e79d643d96cec6 Mon Sep 17 00:00:00 2001 From: yhirano Date: Thu, 10 Mar 2016 09:56:49 -0800 Subject: [PATCH] Move multipart resource handling to core/fetch (1/2) Currently a multipart/x-mixed-replace response is parsed in the content layer and dispatched to clients in blink. It is problematic because the parser calls callbacks in a strange way and special handling code scatters from core/html to content/child. This change adds MultipartImageResourceParser in core/fetch. It is basically copied from multipart_response_delegate. BUG=570608 Review URL: https://codereview.chromium.org/1693183002 Cr-Commit-Position: refs/heads/master@{#380431} --- content/child/blink_platform_impl.cc | 9 + content/child/blink_platform_impl.h | 5 + content/child/web_url_loader_impl.cc | 59 +++ content/child/web_url_loader_impl.h | 6 + third_party/WebKit/Source/core/core.gypi | 3 + .../fetch/MultipartImageResourceParser.cpp | 201 ++++++++ .../core/fetch/MultipartImageResourceParser.h | 94 ++++ .../MultipartImageResourceParserTest.cpp | 479 ++++++++++++++++++ third_party/WebKit/public/platform/Platform.h | 5 + 9 files changed, 861 insertions(+) create mode 100644 third_party/WebKit/Source/core/fetch/MultipartImageResourceParser.cpp create mode 100644 third_party/WebKit/Source/core/fetch/MultipartImageResourceParser.h create mode 100644 third_party/WebKit/Source/core/fetch/MultipartImageResourceParserTest.cpp diff --git a/content/child/blink_platform_impl.cc b/content/child/blink_platform_impl.cc index 24e42fdce41d03..ab31fe7a320916 100644 --- a/content/child/blink_platform_impl.cc +++ b/content/child/blink_platform_impl.cc @@ -467,6 +467,15 @@ bool BlinkPlatformImpl::portAllowed(const blink::WebURL& url) const { return net::IsPortAllowedForScheme(gurl.EffectiveIntPort(), gurl.scheme()); } +bool BlinkPlatformImpl::parseMultipartHeadersFromBody( + const char* bytes, + size_t size, + blink::WebURLResponse* response, + size_t* end) const { + return WebURLLoaderImpl::ParseMultipartHeadersFromBody( + bytes, size, response, end); +} + blink::WebThread* BlinkPlatformImpl::createThread(const char* name) { scoped_ptr thread( new WebThreadImplForWorkerScheduler(name)); diff --git a/content/child/blink_platform_impl.h b/content/child/blink_platform_impl.h index bdf839e255bb55..ab81f269a7dee3 100644 --- a/content/child/blink_platform_impl.h +++ b/content/child/blink_platform_impl.h @@ -96,6 +96,11 @@ class CONTENT_EXPORT BlinkPlatformImpl blink::WebURLError cancelledError(const blink::WebURL& url) const override; bool isReservedIPAddress(const blink::WebString& host) const override; bool portAllowed(const blink::WebURL& url) const override; + bool parseMultipartHeadersFromBody(const char* bytes, + size_t size, + blink::WebURLResponse* response, + size_t* end) const override; + blink::WebThread* createThread(const char* name) override; blink::WebThread* currentThread() override; void recordAction(const blink::UserMetricsAction&) override; diff --git a/content/child/web_url_loader_impl.cc b/content/child/web_url_loader_impl.cc index 88018f9b99d2dd..b4c4f69bebbc30 100644 --- a/content/child/web_url_loader_impl.cc +++ b/content/child/web_url_loader_impl.cc @@ -83,6 +83,17 @@ namespace content { namespace { +// The list of response headers that we do not copy from the original +// response when generating a WebURLResponse for a MIME payload. +const char* const kReplaceHeaders[] = { + "content-type", + "content-length", + "content-disposition", + "content-range", + "range", + "set-cookie" +}; + using HeadersVector = ResourceDevToolsInfo::HeadersVector; // Converts timing data from |load_timing| to the format used by WebKit. @@ -1143,4 +1154,52 @@ void WebURLLoaderImpl::setLoadingTaskRunner( context_->SetWebTaskRunner(make_scoped_ptr(loading_task_runner->clone())); } +// This function is implemented here because it uses net functions. it is +// tested in +// third_party/WebKit/Source/core/fetch/MultipartImageResourceParserTest.cpp. +bool WebURLLoaderImpl::ParseMultipartHeadersFromBody( + const char* bytes, + size_t size, + blink::WebURLResponse* response, + size_t* end) { + int headers_end_pos = + net::HttpUtil::LocateEndOfAdditionalHeaders(bytes, size, 0); + + if (headers_end_pos < 0) + return false; + + *end = headers_end_pos; + // Eat headers and prepend a status line as is required by + // HttpResponseHeaders. + std::string headers("HTTP/1.1 200 OK\r\n"); + headers.append(bytes, headers_end_pos); + + scoped_refptr response_headers = + new net::HttpResponseHeaders( + net::HttpUtil::AssembleRawHeaders(headers.c_str(), headers.size())); + + std::string mime_type; + response_headers->GetMimeType(&mime_type); + response->setMIMEType(WebString::fromUTF8(mime_type)); + + std::string charset; + response_headers->GetCharset(&charset); + response->setTextEncodingName(WebString::fromUTF8(charset)); + + // Copy headers listed in kReplaceHeaders to the response. + for (size_t i = 0; i < arraysize(kReplaceHeaders); ++i) { + std::string name(kReplaceHeaders[i]); + std::string value; + WebString webStringName(WebString::fromLatin1(name)); + size_t iterator = 0; + + response->clearHTTPHeaderField(webStringName); + while (response_headers->EnumerateHeader(&iterator, name, &value)) { + response->addHTTPHeaderField(webStringName, + WebString::fromLatin1(value)); + } + } + return true; +} + } // namespace content diff --git a/content/child/web_url_loader_impl.h b/content/child/web_url_loader_impl.h index 019108aff26302..804d1b0bb82497 100644 --- a/content/child/web_url_loader_impl.h +++ b/content/child/web_url_loader_impl.h @@ -68,6 +68,12 @@ class CONTENT_EXPORT WebURLLoaderImpl int intra_priority_value) override; void setLoadingTaskRunner(blink::WebTaskRunner* loading_task_runner) override; + // This is a utility function for multipart image resources. + static bool ParseMultipartHeadersFromBody(const char* bytes, + size_t size, + blink::WebURLResponse* response, + size_t* end); + private: class Context; class RequestPeerImpl; diff --git a/third_party/WebKit/Source/core/core.gypi b/third_party/WebKit/Source/core/core.gypi index 241cca4588d0eb..3cdb438dddc637 100644 --- a/third_party/WebKit/Source/core/core.gypi +++ b/third_party/WebKit/Source/core/core.gypi @@ -1610,6 +1610,8 @@ 'fetch/LinkFetchResource.h', 'fetch/MemoryCache.cpp', 'fetch/MemoryCache.h', + 'fetch/MultipartImageResourceParser.cpp', + 'fetch/MultipartImageResourceParser.h', 'fetch/RawResource.cpp', 'fetch/RawResource.h', 'fetch/Resource.cpp', @@ -3959,6 +3961,7 @@ 'fetch/ImageResourceTest.cpp', 'fetch/MemoryCacheTest.cpp', 'fetch/MockImageResourceClient.cpp', + 'fetch/MultipartImageResourceParserTest.cpp', 'fetch/RawResourceTest.cpp', 'fetch/ResourceFetcherTest.cpp', 'fetch/ResourceLoaderOptionsTest.cpp', diff --git a/third_party/WebKit/Source/core/fetch/MultipartImageResourceParser.cpp b/third_party/WebKit/Source/core/fetch/MultipartImageResourceParser.cpp new file mode 100644 index 00000000000000..266edfd9b34c2b --- /dev/null +++ b/third_party/WebKit/Source/core/fetch/MultipartImageResourceParser.cpp @@ -0,0 +1,201 @@ +// Copyright 2016 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 "core/fetch/MultipartImageResourceParser.h" + +#include "public/platform/Platform.h" +#include "public/platform/WebURLResponse.h" +#include "wtf/NotFound.h" +#include "wtf/text/WTFString.h" + +#include + +namespace blink { + +MultipartImageResourceParser::MultipartImageResourceParser(const ResourceResponse& response, const Vector& boundary, Client* client) + : m_originalResponse(response) + , m_boundary(boundary) + , m_client(client) +{ + // Some servers report a boundary prefixed with "--". See https://crbug.com/5786. + if (m_boundary.size() < 2 || m_boundary[0] != '-' || m_boundary[1] != '-') + m_boundary.prepend("--", 2); +} + +void MultipartImageResourceParser::appendData(const char* bytes, size_t size) +{ + // m_sawLastBoundary means that we've already received the final boundary + // token. The server should stop sending us data at this point, but if it + // does, we just throw it away. + if (m_sawLastBoundary) + return; + m_data.append(bytes, size); + + if (m_isParsingTop) { + // Eat leading \r\n + size_t pos = pushOverLine(m_data, 0); + if (pos) + m_data.remove(0, pos); + + if (m_data.size() < m_boundary.size() + 2) { + // We don't have enough data yet to make a boundary token. Just + // wait until the next chunk of data arrives. + return; + } + + // Some servers don't send a boundary token before the first chunk of + // data. We handle this case anyway (Gecko does too). + if (0 != memcmp(m_data.data(), m_boundary.data(), m_boundary.size())) { + m_data.prepend("\n", 1); + m_data.prependVector(m_boundary); + } + m_isParsingTop = false; + } + + // Headers + if (m_isParsingHeaders) { + // Eat leading \r\n + size_t pos = pushOverLine(m_data, 0); + if (pos) + m_data.remove(0, pos); + + if (!parseHeaders()) { + // Get more data before trying again. + return; + } + // Successfully parsed headers. + m_isParsingHeaders = false; + if (isCancelled()) + return; + } + + size_t boundaryPosition; + while ((boundaryPosition = findBoundary(m_data, &m_boundary)) != kNotFound) { + // Strip out trailing \r\n characters in the buffer preceding the + // boundary on the same lines as does Firefox. + size_t dataSize = boundaryPosition; + if (boundaryPosition > 0 && m_data[boundaryPosition - 1] == '\n') { + dataSize--; + if (boundaryPosition > 1 && m_data[boundaryPosition - 2] == '\r') { + dataSize--; + } + } + if (dataSize) { + m_client->multipartDataReceived(m_data.data(), dataSize); + if (isCancelled()) + return; + } + size_t boundaryEndPosition = boundaryPosition + m_boundary.size(); + if (boundaryEndPosition < m_data.size() && '-' == m_data[boundaryEndPosition]) { + // This was the last boundary so we can stop processing. + m_sawLastBoundary = true; + m_data.clear(); + return; + } + + // We can now throw out data up through the boundary + size_t offset = pushOverLine(m_data, boundaryEndPosition); + m_data.remove(0, boundaryEndPosition + offset); + + // Ok, back to parsing headers + if (!parseHeaders()) { + m_isParsingHeaders = true; + break; + } + } + + // At this point, we should send over any data we have, but keep enough data + // buffered to handle a boundary that may have been truncated. + if (!m_isParsingHeaders && m_data.size() > m_boundary.size()) { + // If the last character is a new line character, go ahead and just send + // everything we have buffered. This matches an optimization in Gecko. + size_t sendLength = m_data.size() - m_boundary.size(); + if (m_data.last() == '\n') + sendLength = m_data.size(); + m_client->multipartDataReceived(m_data.data(), sendLength); + m_data.remove(0, sendLength); + } +} + +void MultipartImageResourceParser::finish() +{ + ASSERT(!isCancelled()); + // If we have any pending data and we're not in a header, go ahead and send + // it to the client. + if (!m_isParsingHeaders && !m_data.isEmpty() && !m_sawLastBoundary) + m_client->multipartDataReceived(m_data.data(), m_data.size()); + m_data.clear(); + m_sawLastBoundary = true; +} + +size_t MultipartImageResourceParser::pushOverLine(const Vector& data, size_t pos) +{ + size_t offset = 0; + // TODO(yhirano): This function has two problems. Fix them. + // 1. It eats "\n\n". + // 2. When the incoming data is not sufficient (i.e. data[pos] == '\r' + // && data.size() == pos + 1), it should notify the caller. + if (pos < data.size() && (data[pos] == '\r' || data[pos] == '\n')) { + ++offset; + if (pos + 1 < data.size() && data[pos + 1] == '\n') + ++offset; + } + return offset; +} + +bool MultipartImageResourceParser::parseHeaders() +{ + // Create a WebURLResponse based on the original set of headers + the + // replacement headers. We only replace the same few headers that gecko + // does. See netwerk/streamconv/converters/nsMultiMixedConv.cpp. + WebURLResponse response(m_originalResponse.url()); + for (const auto& header : m_originalResponse.httpHeaderFields()) + response.addHTTPHeaderField(header.key, header.value); + + size_t end = 0; + if (!Platform::current()->parseMultipartHeadersFromBody(m_data.data(), m_data.size(), &response, &end)) + return false; + m_data.remove(0, end); + + // To avoid recording every multipart load as a separate visit in + // the history database, we want to keep track of whether the response + // is part of a multipart payload. We do want to record the first visit, + // so we only set isMultipartPayload to true after the first visit. + response.setIsMultipartPayload(!m_isFirstPart); + m_isFirstPart = false; + // Send the response! + m_client->onePartInMultipartReceived(response.toResourceResponse()); + + return true; +} + +// Boundaries are supposed to be preceeded with --, but it looks like gecko +// doesn't require the dashes to exist. See nsMultiMixedConv::FindToken. +size_t MultipartImageResourceParser::findBoundary(const Vector& data, Vector* boundary) +{ + auto it = std::search(data.data(), data.data() + data.size(), boundary->data(), boundary->data() + boundary->size()); + if (it == data.data() + data.size()) + return kNotFound; + + size_t boundaryPosition = it - data.data(); + // Back up over -- for backwards compat + // TODO(tc): Don't we only want to do this once? Gecko code doesn't + // seem to care. + if (boundaryPosition >= 2) { + if (data[boundaryPosition - 1] == '-' && data[boundaryPosition - 2] == '-') { + boundaryPosition -= 2; + Vector v(2, '-'); + v.appendVector(*boundary); + *boundary = v; + } + } + return boundaryPosition; +} + +DEFINE_TRACE(MultipartImageResourceParser) +{ + visitor->trace(m_client); +} + +} // namespace blink diff --git a/third_party/WebKit/Source/core/fetch/MultipartImageResourceParser.h b/third_party/WebKit/Source/core/fetch/MultipartImageResourceParser.h new file mode 100644 index 00000000000000..c812bb96c94688 --- /dev/null +++ b/third_party/WebKit/Source/core/fetch/MultipartImageResourceParser.h @@ -0,0 +1,94 @@ +// Copyright 2016 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. + +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#ifndef MultipartImageResourceParser_h +#define MultipartImageResourceParser_h + +#include "core/CoreExport.h" +#include "platform/heap/Handle.h" +#include "platform/network/ResourceResponse.h" +#include "wtf/Vector.h" + +namespace blink { + +// A parser parsing mlutipart/x-mixed-replace resource. +class CORE_EXPORT MultipartImageResourceParser final : public GarbageCollectedFinalized { + WTF_MAKE_NONCOPYABLE(MultipartImageResourceParser); +public: + class CORE_EXPORT Client : public WillBeGarbageCollectedMixin { + public: + virtual ~Client() = default; + virtual void onePartInMultipartReceived(const ResourceResponse&) = 0; + virtual void multipartDataReceived(const char* bytes, size_t) = 0; + DEFINE_INLINE_VIRTUAL_TRACE() {} + }; + + MultipartImageResourceParser(const ResourceResponse&, const Vector& boundary, Client*); + void appendData(const char* bytes, size_t); + void finish(); + void cancel() { m_isCancelled = true; } + + DECLARE_TRACE(); + + static size_t pushOverLineForTest(const Vector& data, size_t size) { return pushOverLine(data, size); } + static size_t findBoundaryForTest(const Vector& data, Vector* boundary) { return findBoundary(data, boundary); } + +private: + bool parseHeaders(); + bool isCancelled() const { return m_isCancelled; } + static size_t pushOverLine(const Vector&, size_t); + // This function updates |*boundary|. + static size_t findBoundary(const Vector& data, Vector* boundary); + + const ResourceResponse m_originalResponse; + Vector m_boundary; + RawPtrWillBeMember m_client; + + Vector m_data; + bool m_isParsingTop = true; + bool m_isParsingHeaders = false; + bool m_sawLastBoundary = false; + bool m_isFirstPart = true; + bool m_isCancelled = false; +}; + +} // namespace blink + +#endif // MultipartImageResourceParser_h diff --git a/third_party/WebKit/Source/core/fetch/MultipartImageResourceParserTest.cpp b/third_party/WebKit/Source/core/fetch/MultipartImageResourceParserTest.cpp new file mode 100644 index 00000000000000..6fb132fe6b249c --- /dev/null +++ b/third_party/WebKit/Source/core/fetch/MultipartImageResourceParserTest.cpp @@ -0,0 +1,479 @@ +// Copyright 2016 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 "core/fetch/MultipartImageResourceParser.h" + +#include "platform/network/ResourceResponse.h" +#include "public/platform/Platform.h" +#include "public/platform/WebURL.h" +#include "public/platform/WebURLResponse.h" +#include "testing/gtest/include/gtest/gtest.h" + +#include +#include +#include + +namespace blink { + +namespace { + +String toString(const Vector& data) +{ + if (data.isEmpty()) + return String(""); + return String(data.data(), data.size()); +} + +class MockClient final : public NoBaseWillBeGarbageCollectedFinalized, public MultipartImageResourceParser::Client { + WILL_BE_USING_GARBAGE_COLLECTED_MIXIN(MockClient); + +public: + void onePartInMultipartReceived(const ResourceResponse& response) override + { + m_responses.append(response); + m_data.append(Vector()); + } + void multipartDataReceived(const char* bytes, size_t size) override + { + m_data.last().append(bytes, size); + } + + Vector m_responses; + Vector> m_data; +}; + +TEST(MultipartResponseTest, PushOverLine) +{ + struct { + const char* input; + const size_t position; + const size_t expected; + } lineTests[] = { + { "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 < WTF_ARRAY_LENGTH(lineTests); ++i) { + Vector input; + input.append(lineTests[i].input, strlen(lineTests[i].input)); + EXPECT_EQ(lineTests[i].expected, + MultipartImageResourceParser::pushOverLineForTest(input, lineTests[i].position)); + } +} + +TEST(MultipartResponseTest, ParseMultipartHeadersResult) +{ + struct { + const char* data; + const bool result; + const size_t end; + } tests[] = { + { "This is junk", false, 0 }, + { "Foo: bar\nBaz:\n\nAfter:\n", true, 15 }, + { "Foo: bar\nBaz:\n", false, 0}, + { "Foo: bar\r\nBaz:\r\n\r\nAfter:\r\n", true, 18 }, + { "Foo: bar\r\nBaz:\r\n", false, 0 }, + { "Foo: bar\nBaz:\r\n\r\nAfter:\n\n", true, 17 }, + { "Foo: bar\r\nBaz:\n", false, 0 }, + { "\r\n", true, 2 }, + }; + for (size_t i = 0; i < WTF_ARRAY_LENGTH(tests); ++i) { + WebURLResponse response; + response.initialize(); + size_t end = 0; + bool result = Platform::current()->parseMultipartHeadersFromBody(tests[i].data, strlen(tests[i].data), &response, &end); + EXPECT_EQ(tests[i].result, result); + EXPECT_EQ(tests[i].end, end); + } +} + +TEST(MultipartResponseTest, ParseMultipartHeaders) +{ + WebURLResponse webResponse; + webResponse.initialize(); + webResponse.addHTTPHeaderField(WebString::fromLatin1("foo"), WebString::fromLatin1("bar")); + webResponse.addHTTPHeaderField(WebString::fromLatin1("range"), WebString::fromLatin1("piyo")); + webResponse.addHTTPHeaderField(WebString::fromLatin1("content-length"), WebString::fromLatin1("999")); + + const char data[] = "content-type: image/png\ncontent-length: 10\n\n"; + size_t end = 0; + bool result = Platform::current()->parseMultipartHeadersFromBody(data, strlen(data), &webResponse, &end); + const ResourceResponse& response = webResponse.toResourceResponse(); + + EXPECT_TRUE(result); + EXPECT_EQ(strlen(data), end); + EXPECT_EQ("image/png", response.httpHeaderField("content-type")); + EXPECT_EQ("10", response.httpHeaderField("content-length")); + EXPECT_EQ("bar", response.httpHeaderField("foo")); + EXPECT_EQ(AtomicString(), response.httpHeaderField("range")); +} + +TEST(MultipartResponseTest, ParseMultipartHeadersContentCharset) +{ + WebURLResponse webResponse; + webResponse.initialize(); + + const char data[] = "content-type: text/html; charset=utf-8\n\n"; + size_t end = 0; + bool result = Platform::current()->parseMultipartHeadersFromBody(data, strlen(data), &webResponse, &end); + const ResourceResponse& response = webResponse.toResourceResponse(); + + EXPECT_TRUE(result); + EXPECT_EQ(strlen(data), end); + EXPECT_EQ("text/html; charset=utf-8", response.httpHeaderField("content-type")); + EXPECT_EQ("utf-8", response.textEncodingName()); +} + +TEST(MultipartResponseTest, FindBoundary) +{ + struct { + const char* boundary; + const char* data; + const size_t position; + } boundaryTests[] = { + { "bound", "bound", 0 }, + { "bound", "--bound", 0 }, + { "bound", "junkbound", 4 }, + { "bound", "junk--bound", 4 }, + { "foo", "bound", kNotFound }, + { "bound", "--boundbound", 0 }, + }; + + for (size_t i = 0; i < WTF_ARRAY_LENGTH(boundaryTests); ++i) { + Vector boundary, data; + boundary.append(boundaryTests[i].boundary, strlen(boundaryTests[i].boundary)); + data.append(boundaryTests[i].data, strlen(boundaryTests[i].data)); + EXPECT_EQ(boundaryTests[i].position, MultipartImageResourceParser::findBoundaryForTest(data, &boundary)); + } +} + +TEST(MultipartResponseTest, NoStartBoundary) +{ + ResourceResponse response; + response.setMimeType("multipart/x-mixed-replace"); + response.setHTTPHeaderField("Foo", "Bar"); + response.setHTTPHeaderField("Content-type", "text/plain"); + MockClient* client = new MockClient; + Vector boundary; + boundary.append("bound", 5); + + MultipartImageResourceParser* parser = new MultipartImageResourceParser(response, boundary, client); + const char data[] = + "Content-type: text/plain\n\n" + "This is a sample response\n" + "--bound--" + "ignore junk after end token --bound\n\nTest2\n"; + parser->appendData(data, strlen(data)); + ASSERT_EQ(1u, client->m_responses.size()); + ASSERT_EQ(1u, client->m_data.size()); + EXPECT_EQ("This is a sample response", toString(client->m_data[0])); + + parser->finish(); + ASSERT_EQ(1u, client->m_responses.size()); + ASSERT_EQ(1u, client->m_data.size()); + EXPECT_EQ("This is a sample response", toString(client->m_data[0])); +} + +TEST(MultipartResponseTest, NoEndBoundary) +{ + ResourceResponse response; + response.setMimeType("multipart/x-mixed-replace"); + response.setHTTPHeaderField("Foo", "Bar"); + response.setHTTPHeaderField("Content-type", "text/plain"); + MockClient* client = new MockClient; + Vector boundary; + boundary.append("bound", 5); + + MultipartImageResourceParser* parser = new MultipartImageResourceParser(response, boundary, client); + const char data[] = + "bound\nContent-type: text/plain\n\n" + "This is a sample response\n"; + parser->appendData(data, strlen(data)); + ASSERT_EQ(1u, client->m_responses.size()); + ASSERT_EQ(1u, client->m_data.size()); + EXPECT_EQ("This is a sample response\n", toString(client->m_data[0])); + + parser->finish(); + ASSERT_EQ(1u, client->m_responses.size()); + ASSERT_EQ(1u, client->m_data.size()); + EXPECT_EQ("This is a sample response\n", toString(client->m_data[0])); +} + +TEST(MultipartResponseTest, NoStartAndEndBoundary) +{ + ResourceResponse response; + response.setMimeType("multipart/x-mixed-replace"); + response.setHTTPHeaderField("Foo", "Bar"); + response.setHTTPHeaderField("Content-type", "text/plain"); + MockClient* client = new MockClient; + Vector boundary; + boundary.append("bound", 5); + + MultipartImageResourceParser* parser = new MultipartImageResourceParser(response, boundary, client); + const char data[] = + "Content-type: text/plain\n\n" + "This is a sample response\n"; + parser->appendData(data, strlen(data)); + ASSERT_EQ(1u, client->m_responses.size()); + ASSERT_EQ(1u, client->m_data.size()); + EXPECT_EQ("This is a sample response\n", toString(client->m_data[0])); + + parser->finish(); + ASSERT_EQ(1u, client->m_responses.size()); + ASSERT_EQ(1u, client->m_data.size()); + EXPECT_EQ("This is a sample response\n", toString(client->m_data[0])); +} + +TEST(MultipartResponseTest, MalformedBoundary) +{ + // Some servers send a boundary that is prefixed by "--". See bug 5786. + ResourceResponse response; + response.setMimeType("multipart/x-mixed-replace"); + response.setHTTPHeaderField("Foo", "Bar"); + response.setHTTPHeaderField("Content-type", "text/plain"); + MockClient* client = new MockClient; + Vector boundary; + boundary.append("--bound", 7); + + MultipartImageResourceParser* parser = new MultipartImageResourceParser(response, boundary, client); + const char 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"; + parser->appendData(data, strlen(data)); + ASSERT_EQ(1u, client->m_responses.size()); + ASSERT_EQ(1u, client->m_data.size()); + EXPECT_EQ("This is a sample response", toString(client->m_data[0])); + + parser->finish(); + ASSERT_EQ(1u, client->m_responses.size()); + ASSERT_EQ(1u, client->m_data.size()); + EXPECT_EQ("This is a sample response", toString(client->m_data[0])); +} + +// Used in for tests that break the data in various places. +struct TestChunk { + const int startPosition; // offset in data + const int endPosition; // end offset in data + const size_t expectedResponses; + const char* expectedData; +}; + +void variousChunkSizesTest(const TestChunk chunks[], int chunksSize, + size_t responses, int receivedData, + const char* completedData) +{ + const char 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 + + ResourceResponse response; + response.setMimeType("multipart/x-mixed-replace"); + MockClient* client = new MockClient; + Vector boundary; + boundary.append("bound", 5); + + MultipartImageResourceParser* parser = new MultipartImageResourceParser(response, boundary, client); + + for (int i = 0; i < chunksSize; ++i) { + ASSERT_LT(chunks[i].startPosition, chunks[i].endPosition); + parser->appendData(data + chunks[i].startPosition, chunks[i].endPosition - chunks[i].startPosition); + EXPECT_EQ(chunks[i].expectedResponses, client->m_responses.size()); + EXPECT_EQ(String(chunks[i].expectedData), client->m_data.size() > 0 ? toString(client->m_data.last()) : String("")); + } + // Check final state + parser->finish(); + EXPECT_EQ(responses, client->m_responses.size()); + EXPECT_EQ(completedData, toString(client->m_data.last())); +} + +template +void variousChunkSizesTest(const TestChunk (&chunks)[N], size_t responses, int receivedData, const char* completedData) +{ + variousChunkSizesTest(chunks, N, responses, receivedData, completedData); +} + +TEST(MultipartResponseTest, BreakInBoundary) +{ + // Break in the first boundary + const TestChunk bound1[] = { + { 0, 4, 0, "" }, + { 4, 110, 2, "foofoofoofoofoo" }, + }; + variousChunkSizesTest(bound1, 2, 2, "foofoofoofoofoo"); + + // Break in first and second + const TestChunk bound2[] = { + { 0, 4, 0, "" }, + { 4, 55, 1, "datadatadatadat" }, + { 55, 65, 1, "datadatadatadatadata" }, + { 65, 110, 2, "foofoofoofoofoo" }, + }; + variousChunkSizesTest(bound2, 2, 3, "foofoofoofoofoo"); + + // Break in second only + const TestChunk bound3[] = { + { 0, 55, 1, "datadatadatadat" }, + { 55, 110, 2, "foofoofoofoofoo" }, + }; + variousChunkSizesTest(bound3, 2, 3, "foofoofoofoofoo"); +} + +TEST(MultipartResponseTest, BreakInHeaders) +{ + // Break in first header + const TestChunk header1[] = { + { 0, 10, 0, "" }, + { 10, 35, 1, "" }, + { 35, 110, 2, "foofoofoofoofoo" }, + }; + variousChunkSizesTest(header1, 2, 2, "foofoofoofoofoo"); + + // Break in both headers + const TestChunk header2[] = { + { 0, 10, 0, "" }, + { 10, 65, 1, "datadatadatadatadata" }, + { 65, 110, 2, "foofoofoofoofoo" }, + }; + variousChunkSizesTest(header2, 2, 2, "foofoofoofoofoo"); + + // Break at end of a header + const TestChunk header3[] = { + { 0, 33, 1, "" }, + { 33, 65, 1, "datadatadatadatadata" }, + { 65, 110, 2, "foofoofoofoofoo" }, + }; + variousChunkSizesTest(header3, 2, 2, "foofoofoofoofoo"); +} + +TEST(MultipartResponseTest, BreakInData) +{ + // All data as one chunk + const TestChunk data1[] = { + { 0, 110, 2, "foofoofoofoofoo" }, + }; + variousChunkSizesTest(data1, 2, 2, "foofoofoofoofoo"); + + // breaks in data segment + const TestChunk data2[] = { + { 0, 35, 1, "" }, + { 35, 65, 1, "datadatadatadatadata" }, + { 65, 90, 2, "" }, + { 90, 110, 2, "foofoofoofoofoo" }, + }; + variousChunkSizesTest(data2, 2, 2, "foofoofoofoofoo"); + + // Incomplete send + const TestChunk data3[] = { + { 0, 35, 1, "" }, + { 35, 90, 2, "" }, + }; + variousChunkSizesTest(data3, 2, 2, "foof"); +} + +TEST(MultipartResponseTest, SmallChunk) +{ + ResourceResponse response; + response.setMimeType("multipart/x-mixed-replace"); + response.setHTTPHeaderField("Content-type", "text/plain"); + MockClient* client = new MockClient; + Vector boundary; + boundary.append("bound", 5); + + MultipartImageResourceParser* parser = new MultipartImageResourceParser(response, boundary, client); + + // Test chunks of size 1, 2, and 0. + const char 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--"; + parser->appendData(data, strlen(data)); + ASSERT_EQ(4u, client->m_responses.size()); + ASSERT_EQ(4u, client->m_data.size()); + EXPECT_EQ("", toString(client->m_data[0])); + EXPECT_EQ("\n", toString(client->m_data[1])); + EXPECT_EQ("", toString(client->m_data[2])); + EXPECT_EQ("end", toString(client->m_data[3])); + + parser->finish(); + ASSERT_EQ(4u, client->m_responses.size()); + ASSERT_EQ(4u, client->m_data.size()); + EXPECT_EQ("", toString(client->m_data[0])); + EXPECT_EQ("\n", toString(client->m_data[1])); + EXPECT_EQ("", toString(client->m_data[2])); + EXPECT_EQ("end", toString(client->m_data[3])); +} + +TEST(MultipartResponseTest, MultipleBoundaries) +{ + // Test multiple boundaries back to back + ResourceResponse response; + response.setMimeType("multipart/x-mixed-replace"); + MockClient* client = new MockClient; + Vector boundary; + boundary.append("bound", 5); + + MultipartImageResourceParser* parser = new MultipartImageResourceParser(response, boundary, client); + + const char data[] = "--bound\r\n\r\n--bound\r\n\r\nfoofoo--bound--"; + parser->appendData(data, strlen(data)); + ASSERT_EQ(2u, client->m_responses.size()); + ASSERT_EQ(2u, client->m_data.size()); + EXPECT_EQ("", toString(client->m_data[0])); + EXPECT_EQ("foofoo", toString(client->m_data[1])); +} + +TEST(MultipartResponseTest, MultipartPayloadSet) +{ + ResourceResponse response; + response.setMimeType("multipart/x-mixed-replace"); + MockClient* client = new MockClient; + Vector boundary; + boundary.append("bound", 5); + + MultipartImageResourceParser* parser = new MultipartImageResourceParser(response, boundary, client); + + const char data[] = + "--bound\n" + "Content-type: text/plain\n\n" + "response data\n" + "--bound\n"; + parser->appendData(data, strlen(data)); + ASSERT_EQ(1u, client->m_responses.size()); + ASSERT_EQ(1u, client->m_data.size()); + EXPECT_EQ("response data", toString(client->m_data[0])); + EXPECT_FALSE(client->m_responses[0].isMultipartPayload()); + + const char data2[] = + "Content-type: text/plain\n\n" + "response data2\n" + "--bound\n"; + parser->appendData(data2, strlen(data2)); + ASSERT_EQ(2u, client->m_responses.size()); + ASSERT_EQ(2u, client->m_data.size()); + EXPECT_EQ("response data2", toString(client->m_data[1])); + EXPECT_TRUE(client->m_responses[1].isMultipartPayload()); +} + +} // namespace + +} // namespace blink diff --git a/third_party/WebKit/public/platform/Platform.h b/third_party/WebKit/public/platform/Platform.h index a586de4beeb432..880be7b063edc5 100644 --- a/third_party/WebKit/public/platform/Platform.h +++ b/third_party/WebKit/public/platform/Platform.h @@ -120,6 +120,7 @@ class WebThread; class WebTrialTokenValidator; class WebURL; class WebURLLoader; +class WebURLResponse; class WebUnitTestSupport; struct WebLocalizedString; struct WebSize; @@ -325,6 +326,10 @@ class BLINK_PLATFORM_EXPORT Platform { virtual bool portAllowed(const WebURL&) const { return false; } + // Returns true and stores the position of the end of the headers to |*end| + // if the headers part ends in |bytes[0..size]|. Returns false otherwise. + virtual bool parseMultipartHeadersFromBody(const char* bytes, size_t /* size */, WebURLResponse*, size_t* end) const { return false; } + // Plugins ------------------------------------------------------------- // If refresh is true, then cached information should not be used to