// Copyright 2012 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 "base/mac/scoped_nsobject.h" #include "base/memory/scoped_ptr.h" #include "base/message_loop/message_loop.h" #include "base/run_loop.h" #include "base/strings/sys_string_conversions.h" #import "ios/net/protocol_handler_util.h" #include "net/base/elements_upload_data_stream.h" #import "net/base/mac/url_conversions.h" #include "net/base/upload_bytes_element_reader.h" #include "net/http/http_request_headers.h" #include "net/http/http_response_headers.h" #include "net/url_request/data_protocol_handler.h" #include "net/url_request/url_request.h" #include "net/url_request/url_request_job.h" #include "net/url_request/url_request_job_factory.h" #include "net/url_request/url_request_job_factory_impl.h" #include "net/url_request/url_request_test_util.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest_mac.h" #include "url/gurl.h" // When C++ exceptions are disabled, the C++ library defines |try| and // |catch| so as to allow exception-expecting C++ code to build properly when // language support for exceptions is not present. These macros interfere // with the use of |@try| and |@catch| in Objective-C files such as this one. // Undefine these macros here, after everything has been #included, since // there will be no C++ uses and only Objective-C uses from this point on. #undef try #undef catch namespace net { namespace { const int kResponseCode = 200; const char* kTextHtml = "text/html"; const char* kTextPlain = "text/plain"; const char* kAscii = "US-ASCII"; class HeadersURLRequestJob : public URLRequestJob { public: HeadersURLRequestJob(URLRequest* request) : URLRequestJob(request, nullptr) {} void Start() override { // Fills response headers and returns immediately. NotifyHeadersComplete(); } bool GetMimeType(std::string* mime_type) const override { *mime_type = GetContentTypeValue(); return true; } void GetResponseInfo(HttpResponseInfo* info) override { // This is called by NotifyHeadersComplete(). std::string header_string("HTTP/1.0 200 OK"); header_string.push_back('\0'); header_string += std::string("Cache-Control: max-age=600"); header_string.push_back('\0'); if (request()->url().DomainIs("multiplecontenttype")) { header_string += std::string( "coNteNt-tYPe: text/plain; charset=iso-8859-4, image/png"); header_string.push_back('\0'); } header_string += std::string("Content-Type: ") + GetContentTypeValue(); header_string.push_back('\0'); header_string += std::string("Foo: A"); header_string.push_back('\0'); header_string += std::string("Bar: B"); header_string.push_back('\0'); header_string += std::string("Baz: C"); header_string.push_back('\0'); header_string += std::string("Foo: D"); header_string.push_back('\0'); header_string += std::string("Foo: E"); header_string.push_back('\0'); header_string += std::string("Bar: F"); header_string.push_back('\0'); info->headers = new HttpResponseHeaders(header_string); } int GetResponseCode() const override { return kResponseCode; } protected: ~HeadersURLRequestJob() override {} std::string GetContentTypeValue() const { if (request()->url().DomainIs("badcontenttype")) return "\xff"; return kTextHtml; } }; class NetProtocolHandler : public URLRequestJobFactory::ProtocolHandler { public: URLRequestJob* MaybeCreateJob( URLRequest* request, NetworkDelegate* network_delegate) const override { return new HeadersURLRequestJob(request); } }; class ProtocolHandlerUtilTest : public testing::Test, public URLRequest::Delegate { public: ProtocolHandlerUtilTest() : request_context_(new TestURLRequestContext) { // Ownership of the protocol handlers is transferred to the factory. job_factory_.SetProtocolHandler("http", new NetProtocolHandler); job_factory_.SetProtocolHandler("data", new DataProtocolHandler); request_context_->set_job_factory(&job_factory_); } NSURLResponse* BuildDataURLResponse(const std::string& mime_type, const std::string& encoding, const std::string& content) { // Build an URL in the form "data:;charset=," // The ';' is removed if mime_type or charset is empty. std::string url_string = std::string("data:") + mime_type; if (!encoding.empty()) url_string += ";charset=" + encoding; url_string += ","; GURL url(url_string); scoped_ptr request( request_context_->CreateRequest(url, DEFAULT_PRIORITY, this)); request->Start(); base::RunLoop loop; loop.RunUntilIdle(); return GetNSURLResponseForRequest(request.get()); } void CheckDataResponse(NSURLResponse* response, const std::string& mime_type, const std::string& encoding) { EXPECT_NSEQ(base::SysUTF8ToNSString(mime_type), [response MIMEType]); EXPECT_NSEQ(base::SysUTF8ToNSString(encoding), [response textEncodingName]); // The response class must be NSURLResponse (and not NSHTTPURLResponse) when // the scheme is "data". EXPECT_TRUE([response isMemberOfClass:[NSURLResponse class]]); } void OnResponseStarted(URLRequest* request) override {} void OnReadCompleted(URLRequest* request, int bytes_read) override {} protected: base::MessageLoop loop_; URLRequestJobFactoryImpl job_factory_; scoped_ptr request_context_; }; } // namespace TEST_F(ProtocolHandlerUtilTest, GetResponseDataSchemeTest) { NSURLResponse* response; // MIME type and charset are correctly carried over. response = BuildDataURLResponse("#mime=type'", "$(charset-*", "content"); CheckDataResponse(response, "#mime=type'", "$(charset-*"); // Missing values are treated as default values. response = BuildDataURLResponse("", "", "content"); CheckDataResponse(response, kTextPlain, kAscii); } TEST_F(ProtocolHandlerUtilTest, GetResponseHttpTest) { // Create a request. GURL url(std::string("http://url")); scoped_ptr request( request_context_->CreateRequest(url, DEFAULT_PRIORITY, this)); request->Start(); // Create a response from the request. NSURLResponse* response = GetNSURLResponseForRequest(request.get()); EXPECT_NSEQ([NSString stringWithUTF8String:kTextHtml], [response MIMEType]); ASSERT_TRUE([response isKindOfClass:[NSHTTPURLResponse class]]); NSHTTPURLResponse* http_response = (NSHTTPURLResponse*)response; NSDictionary* headers = [http_response allHeaderFields]; // Check the headers, duplicates must be appended. EXPECT_EQ(5u, [headers count]); NSString* foo_header = [headers objectForKey:@"Foo"]; EXPECT_NSEQ(@"A,D,E", foo_header); NSString* bar_header = [headers objectForKey:@"Bar"]; EXPECT_NSEQ(@"B,F", bar_header); NSString* baz_header = [headers objectForKey:@"Baz"]; EXPECT_NSEQ(@"C", baz_header); NSString* cache_header = [headers objectForKey:@"Cache-Control"]; EXPECT_NSEQ(@"no-store", cache_header); // Cache-Control is overridden. // Check the status. EXPECT_EQ(request->GetResponseCode(), [http_response statusCode]); } TEST_F(ProtocolHandlerUtilTest, BadHttpContentType) { // Create a request using the magic domain that triggers a garbage // content-type in the test framework. GURL url(std::string("http://badcontenttype")); scoped_ptr request( request_context_->CreateRequest(url, DEFAULT_PRIORITY, this)); request->Start(); // Create a response from the request. @try { GetNSURLResponseForRequest(request.get()); } @catch (id exception) { FAIL() << "Exception while creating response"; } } TEST_F(ProtocolHandlerUtilTest, MultipleHttpContentType) { // Create a request using the magic domain that triggers a garbage // content-type in the test framework. GURL url(std::string("http://multiplecontenttype")); scoped_ptr request( request_context_->CreateRequest(url, DEFAULT_PRIORITY, this)); request->Start(); // Create a response from the request. NSURLResponse* response = GetNSURLResponseForRequest(request.get()); EXPECT_NSEQ(@"text/plain", [response MIMEType]); EXPECT_NSEQ(@"iso-8859-4", [response textEncodingName]); NSHTTPURLResponse* http_response = (NSHTTPURLResponse*)response; NSDictionary* headers = [http_response allHeaderFields]; NSString* content_type_header = [headers objectForKey:@"Content-Type"]; EXPECT_NSEQ(@"text/plain; charset=iso-8859-4", content_type_header); } TEST_F(ProtocolHandlerUtilTest, CopyHttpHeaders) { GURL url(std::string("http://url")); base::scoped_nsobject in_request( [[NSMutableURLRequest alloc] initWithURL:NSURLWithGURL(url)]); [in_request setAllHTTPHeaderFields:@{ @"Referer" : @"referrer", @"User-Agent" : @"secret", @"Accept" : @"money/cash", @"Foo" : @"bar", }]; scoped_ptr out_request( request_context_->CreateRequest(url, DEFAULT_PRIORITY, nullptr)); CopyHttpHeaders(in_request, out_request.get()); EXPECT_EQ("referrer", out_request->referrer()); const HttpRequestHeaders& headers = out_request->extra_request_headers(); EXPECT_FALSE(headers.HasHeader("User-Agent")); // User agent is not copied. EXPECT_FALSE(headers.HasHeader("Content-Type")); // Only in POST requests. std::string header; EXPECT_TRUE(headers.GetHeader("Accept", &header)); EXPECT_EQ("money/cash", header); EXPECT_TRUE(headers.GetHeader("Foo", &header)); EXPECT_EQ("bar", header); } TEST_F(ProtocolHandlerUtilTest, AddMissingHeaders) { GURL url(std::string("http://url")); base::scoped_nsobject in_request( [[NSMutableURLRequest alloc] initWithURL:NSURLWithGURL(url)]); scoped_ptr out_request( request_context_->CreateRequest(url, DEFAULT_PRIORITY, nullptr)); out_request->set_method("POST"); scoped_ptr reader( new UploadBytesElementReader(nullptr, 0)); out_request->set_upload( ElementsUploadDataStream::CreateWithReader(reader.Pass(), 0)); CopyHttpHeaders(in_request, out_request.get()); // Some headers are added by default if missing. const HttpRequestHeaders& headers = out_request->extra_request_headers(); std::string header; EXPECT_TRUE(headers.GetHeader("Accept", &header)); EXPECT_EQ("*/*", header); EXPECT_TRUE(headers.GetHeader("Content-Type", &header)); EXPECT_EQ("application/x-www-form-urlencoded", header); } } // namespace net