1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "ios/web/net/clients/crw_js_injection_network_client.h"
7 #include "base/logging.h"
8 #include "base/mac/objc_property_releaser.h"
9 #include "base/mac/scoped_nsobject.h"
10 #include "base/metrics/histogram.h"
11 #import "ios/net/crn_http_url_response.h"
12 #import "ios/third_party/blink/src/html_tokenizer.h"
14 // CRWJSInjectionNetworkClient injects an external script tag reference for
15 // crweb.js into HTML and XHTML documents. To do this correctly, three data
16 // points are needed: where to inject the script tag, what encoding the content
17 // is in (ASCII compatible, UTF32, UTF16 and big or little endian) and the byte
18 // length of the injection tag itself.
20 // The content encoding is handled first. As data is received,
21 // CRWJSInjectionNetworkClient will look at the beginning few bytes of the
22 // content for either a byte-order mark or an XML declaration. If present, they
23 // will be matched to a known set of patterns to determine the encoding. If not
24 // present, by definition, the content is in an ASCII compatible encoding (at
25 // least with regard to the markup tags themselves, which is all
26 // CRWJSInjectionNetworkClient cares about).
28 // Next, CRWJSInjectionNetworkClient will look for the byte offset at which to
29 // inject. For HTML and XHTML documents to work correctly, the script tag must
30 // be injected after the "<html>" tag (right after the '>' character). To
31 // determine the byte offset of this character, blink's HTMLTokenizer is used.
32 // Specifically, as data is received from the network,
33 // CRWJSInjectionNetworkClient will buffer the data until a sufficient amount of
34 // data is collected to scan for the "<html>" tag. This scan is repeated, as
35 // data is received, until one of three things happens: 1) an "<html>" tag is
36 // found, 2) some tag OTHER than an "<html>" tag is found, or 3) no tag at all
37 // is found within the first 1024 bytes of the content.
39 // For case 3) above, nothing is injected and the NSURLHTTPResponse as well as
40 // all associated data are immediately dispatched in unaltered form to their
41 // intended recipient (WebKit). All subsequently received data is simply passed
42 // along to its destination unaltered.
44 // For cases 1) & 2) above, the "Content-Length" header of the NSURLHTTPResponse
45 // object will be updated as appropriate (since NSURLHTTPResponse is unmutable,
46 // this means a copy is made) and then dispatched. Immediately after this, the
47 // data is dispatched in three chunks:
48 // - Everything before the injection byte offset.
49 // - The appropriately content encoded <script> tag string.
50 // - All remaining data.
52 // All subsequently received data is simply passed along to its destination
58 // When looking for the meta-charset tag, blink/webkit
59 // limits itself to the first 1024 bytes.
60 const size_t kLimitOfBytesToCheckForHeadTag = 1024;
61 const size_t kMinimumBytesNeededForHTMLTag = 7;
62 const size_t kMinimumBytesNeededForBOM = 4;
63 const size_t kMinimumBytesNeededForXMLDecl = 8;
65 NSString* const kContentLength = @"Content-Length";
67 NSString* const kJSContentTemplate =
68 @"<script src=\"%@_crweb.js\" charset=\"utf-8\"></script>";
70 // Compares a value pointed to by |bytes| to a single value |byte|. Returns
71 // true if they are equal, false otherwise. This is a base case for the
72 // variadic template version of the method of the same name below.
73 template <typename T1, typename T2>
74 bool BytesEqual(const T1* bytes, T2 byte) {
76 return (*bytes == byte);
79 // Compares an array of values |bytes| to a value |byte| and a variable number
80 // of other values specified by |args|. Returns true if the values pointed to
81 // by |bytes| are equal to values |byte| through |args| in positional order,
82 // false otherwise. This means that if true is returned, values bytes[0...N] and
83 // byte...args are all equal.
84 template <typename T1, typename T2, typename... Arguments>
85 bool BytesEqual(const T1* bytes, T2 byte, Arguments... args) {
88 return BytesEqual(++bytes, args...);
93 // Gets the value corresponding to the key "Content-Length" in the passed
94 // |allHeaders| dictionary. If no such key is present in |allHeaders| returns
96 long long GetContentLengthFromAllHeaders(NSDictionary* all_headers) {
97 NSObject* content_length_object = [all_headers objectForKey:kContentLength];
99 // This handles both the case when the object is an NSNumber and the case
100 // when the object is an NSString.
101 if ([content_length_object respondsToSelector:@selector(longLongValue)])
102 return [static_cast<id>(content_length_object) longLongValue];
107 // Returns an CRNHTTPURLResponse instance with the "Content-Length"
108 // increased to include the passed |additionContentSize|. If
109 // |additionContentSize| is zero, the passed response is returned.
110 CRNHTTPURLResponse* ResponseWithUpdatedContentSize(
111 CRNHTTPURLResponse* response,
112 NSUInteger addition_content_size) {
116 if (!addition_content_size)
119 if (![response isKindOfClass:[CRNHTTPURLResponse class]]) {
124 // NSURLResponse uses a long long return type for expectedContentLength.
125 NSDictionary* all_headers = [response allHeaderFields];
126 long long content_length = GetContentLengthFromAllHeaders(all_headers);
127 if (content_length < 0) {
131 // Create a new content length value.
132 content_length += addition_content_size;
133 NSString* content_length_value =
134 [[NSNumber numberWithLongLong:content_length] stringValue];
136 base::scoped_nsobject<NSMutableDictionary> all_headers_mutable;
137 all_headers_mutable.reset([all_headers mutableCopy]);
138 [all_headers_mutable setObject:content_length_value forKey:kContentLength];
140 CRNHTTPURLResponse* update_response =
141 [[CRNHTTPURLResponse alloc] initWithURL:[response URL]
142 statusCode:[response statusCode]
143 HTTPVersion:[response cr_HTTPVersion]
144 headerFields:all_headers_mutable];
146 return [update_response autorelease];
150 @interface CRWJSInjectionNetworkClient () {
151 // The CRNHTTPURLResponse that is held until it is determined if the content
152 // will have JavaScript injected.
153 base::scoped_nsobject<CRNHTTPURLResponse> _pendingResponse;
155 // An array of data that is buffered until a determination is made about
156 // injecting JavaScript.
157 base::scoped_nsobject<NSMutableArray> _pendingData;
159 // The content that will be injected in NSData form.
160 base::scoped_nsobject<NSData> _jsInjectionContent;
162 BOOL _completedByteOrderMarkCheck;
163 BOOL _completedXMLDeclarationCheck;
164 BOOL _completedCheckForWhereToInject;
166 NSStringEncoding _contentEncoding;
167 NSUInteger _headerLength;
168 NSUInteger _pendingDataLength;
170 NSUInteger _byteOffsetAtWhichToInject;
171 BOOL _proceedWithInjection;
174 // Returns YES if all checks (BOM, XML declaration, injection location) are
176 - (BOOL)completedAllChecks;
178 // Records a UMA histogram indicating the injection result
179 // (see enum InjectionResult).
180 - (void)recordHistogramResult;
182 // Returns an NSData containing the correctly encoded script tag referencing
183 // the appropriate crweb.js for this web view.
184 - (NSData*)jsInjectionContent;
186 // If injection is appropriate for this content, dispatches buffered data in
188 // 1) [everything PRIOR to injection byte offset]
189 // 2) [self jsInjectionContent]
190 // 3) [everything AFTER the injection byte offset]
191 - (void)sendInjectedResponseIfNeeded;
193 // Dispatches the buffered and appropriately "Content-Length"-header-updated
194 // NSURLHTTPResponse to the next level network client (almost certainly
195 // WebKit/CFNetwork itself).
196 - (void)sendPendingResponse;
198 // Calls [self sendInjectedResponseIfNeeded] and then sends any remaining
199 // buffered data to the next level network client.
200 - (void)sendPendingData;
202 // Coalesces data contained in self.pendingData until a single NSData object of
203 // at least length |lengthNeeded| has been created and inserted into
204 // self.pendingData as the first element.
205 - (void)coalesceDataIfNeeded:(NSUInteger)lengthNeeded;
207 // Looks for a byte order mark in the content to indicate character encoding.
208 // Sets _completedByteOrderMarkCheck to YES once complete and updates
209 // _contentEncoding if an encoding has been determined.
210 - (void)checkForByteOrderMark;
212 // Looks for an XML declaration in the content to indicate character encoding.
213 // Sets _completedXMLDeclarationCheck to YES once complete and updates
214 // _contentEncoding if an encoding has been determined.
215 - (void)checkForXMLDeclaration;
217 // Checks whether the hard byte limit specified by
218 // kLimitOfBytesToCheckForHeadTag has been hit or exceeded during the check
219 // for an appropriate injection location. If the limit is hit or exceeded,
220 // _completedCheckForWhereToInject will be set to YES and
221 // -[CRWJSInjectionNetworkClient checkForWhereToInject will stop.
222 - (void)checkIfByteLimitPassed:(const WebCore::CharacterProvider&)provider;
224 // Checks for an appropriate byte offset at which to inject the external
225 // script tag. Sets _completedCheckForWhereToInject to YES once complete and
226 // updates _byteOffsetAtWhichToInject to the location at which to inject. Since
227 // this may be set to zero, _proceedWithInjection is set to YES to indicate that
228 // a location at which to inject has been found. Uses blink's HTMLTokenizer.
229 - (void)checkForWhereToInject;
232 @implementation CRWJSInjectionNetworkClient
234 + (BOOL)canHandleResponse:(NSURLResponse*)response {
235 NSString* scheme = [[response URL] scheme];
236 if (![scheme isEqualToString:@"http"] && ![scheme isEqualToString:@"https"])
239 NSString* mimeType = [response MIMEType];
241 // TODO(jyquinn): WebKit has a DOM for any XML document. Should we inject
242 // JavaScript into them and not limit to html and xhtml?
243 if ([mimeType isEqualToString:@"text/html"])
246 if ([mimeType isEqualToString:@"application/xhtml+xml"])
252 #pragma mark CRNNetworkClientProtocol implementation
254 - (void)didFailWithNSErrorCode:(NSInteger)nsErrorCode
255 netErrorCode:(int)netErrorCode {
256 _proceedWithInjection = NO;
257 [self sendPendingResponse];
258 [self sendPendingData];
259 [super didFailWithNSErrorCode:nsErrorCode netErrorCode:netErrorCode];
262 - (void)didLoadData:(NSData*)data {
263 if ([self completedAllChecks]) {
264 [super didLoadData:data];
269 _pendingData.reset([[NSMutableArray alloc] init]);
271 [_pendingData addObject:data];
272 _pendingDataLength += [data length];
274 [self checkForByteOrderMark];
275 [self checkForXMLDeclaration];
276 [self checkForWhereToInject];
278 if ([self completedAllChecks]) {
279 if (!_contentEncoding)
280 _contentEncoding = NSASCIIStringEncoding;
281 [self sendPendingResponse];
282 [self sendPendingData];
286 - (void)didReceiveResponse:(NSURLResponse*)response {
287 DCHECK([response isKindOfClass:[CRNHTTPURLResponse class]]);
288 DCHECK([response expectedContentLength] ==
289 GetContentLengthFromAllHeaders(
290 [static_cast<NSHTTPURLResponse*>(response) allHeaderFields]));
291 DCHECK([CRWJSInjectionNetworkClient canHandleResponse:response]);
293 // If response does not come with Content-Length header field (i.e. the
294 // Transfer-Encoding header field has value 'chunked'), response does not have
296 if ([response expectedContentLength] == -1) {
297 [super didReceiveResponse:response];
299 // Client calls [super didReceiveResponse:] in sendPendingResponse.
300 _pendingResponse.reset([static_cast<CRNHTTPURLResponse*>(response) retain]);
304 - (void)didFinishLoading {
305 [self recordHistogramResult];
306 [self sendPendingResponse];
307 [self sendPendingData];
308 [super didFinishLoading];
311 #pragma mark Internal methods
313 - (BOOL)completedAllChecks {
314 if (!_completedByteOrderMarkCheck)
317 if (!_completedXMLDeclarationCheck)
320 if (!_completedCheckForWhereToInject)
326 - (void)recordHistogramResult {
327 web::InjectionResult result = web::InjectionResult::INJECTION_RESULT_COUNT;
328 if (_proceedWithInjection)
329 result = web::InjectionResult::SUCCESS_INJECTED;
330 else if (_pendingDataLength < kMinimumBytesNeededForHTMLTag)
331 result = web::InjectionResult::FAIL_INSUFFICIENT_CONTENT_LENGTH;
333 result = web::InjectionResult::FAIL_FIND_INJECTION_LOCATION;
335 if (result < web::InjectionResult::INJECTION_RESULT_COUNT) {
336 UMA_HISTOGRAM_ENUMERATION(
337 "NetworkLayerJSInjection.Result", static_cast<int>(result),
338 static_cast<int>(web::InjectionResult::INJECTION_RESULT_COUNT));
344 - (NSData*)jsInjectionContent {
345 DCHECK(_proceedWithInjection);
346 if (_jsInjectionContent)
347 return _jsInjectionContent;
349 NSString* jsContentString = [NSString
350 stringWithFormat:kJSContentTemplate, [[NSUUID UUID] UUIDString]];
351 _jsInjectionContent.reset(
352 [[jsContentString dataUsingEncoding:_contentEncoding] retain]);
354 return _jsInjectionContent;
357 - (void)sendInjectedResponseIfNeeded {
358 if (!_proceedWithInjection)
361 NSData* firstData = [_pendingData firstObject];
362 NSUInteger dataLength = [firstData length];
366 const uint8* bytes = reinterpret_cast<const uint8*>([firstData bytes]);
368 // Construct one data in which to send the content + injected script tag.
369 base::scoped_nsobject<NSMutableData> combined([[NSMutableData alloc] init]);
370 if (_byteOffsetAtWhichToInject)
371 [combined appendBytes:static_cast<const void*>(bytes)
372 length:_byteOffsetAtWhichToInject];
374 // Send back the JavaScript content to inject.
375 [combined appendData:[self jsInjectionContent]];
377 appendBytes:static_cast<const void*>(bytes + _byteOffsetAtWhichToInject)
378 length:(dataLength - _byteOffsetAtWhichToInject)];
380 [super didLoadData:combined];
382 // The first data, into which the JS was injected, is no longer needed.
383 [_pendingData.get() removeObjectAtIndex:0];
384 _jsInjectionContent.reset();
387 - (void)sendPendingResponse {
388 if (!_pendingResponse)
391 if (_proceedWithInjection) {
392 NSUInteger additionalLength = [[self jsInjectionContent] length];
393 CRNHTTPURLResponse* responseToSend =
394 ResponseWithUpdatedContentSize(_pendingResponse, additionalLength);
395 _pendingResponse.reset([responseToSend retain]);
398 [super didReceiveResponse:_pendingResponse];
399 _pendingResponse.reset();
402 - (void)sendPendingData {
403 if (![_pendingData count])
406 [self sendInjectedResponseIfNeeded];
408 for (NSData* data in _pendingData.get())
409 [super didLoadData:data];
411 _pendingData.reset();
414 - (void)coalesceDataIfNeeded:(NSUInteger)lengthNeeded {
415 NSData* firstData = [_pendingData firstObject];
417 // Obviously if the first data object has enough data,
418 // nothing needs to be done.
419 if ([firstData length] >= lengthNeeded)
422 // Make sure we have something that can be coalesced.
423 if ([_pendingData count] < 2)
426 // Start with a mutable copy of the first data item.
427 base::scoped_nsobject<NSMutableData> coalescedData([firstData mutableCopy]);
429 // Replace the first item in the array.
430 [_pendingData removeObjectAtIndex:0];
432 // While not enough data has been coalesced and there are items
433 // that can be coalesced, do so.
434 while ([coalescedData length] < lengthNeeded && [_pendingData count]) {
435 [coalescedData appendData:_pendingData[0]];
436 [_pendingData removeObjectAtIndex:0];
439 // Finally, put the coalesced object at the front of the pending data array.
440 [_pendingData insertObject:coalescedData atIndex:0];
443 - (void)checkForByteOrderMark {
444 // Bail if the check has already been completed.
445 if (_completedByteOrderMarkCheck)
448 // Bail if there is not yet enough data to check.
449 if (_pendingDataLength < kMinimumBytesNeededForBOM)
452 // It's possible that the server returned data in small chunks, so coalesce
453 // the data into one buffer if needed.
454 [self coalesceDataIfNeeded:kMinimumBytesNeededForBOM];
456 // Get some data to investigate.
457 NSData* firstData = [_pendingData firstObject];
458 if (!firstData && [firstData length] < kMinimumBytesNeededForBOM) {
463 // Do the same check that WebKit does for the byte order mark (BOM), which
464 // must be right at the beginning of the content to be accepted.
465 // Info on byte order mark: http://en.wikipedia.org/wiki/Byte_order_mark
466 const uint8* bytes = reinterpret_cast<const uint8*>([firstData bytes]);
467 if (BytesEqual(bytes, 0xFF, 0xFE)) {
470 if (!BytesEqual(bytes, 0x00, 0x00)) {
471 _contentEncoding = NSUTF16LittleEndianStringEncoding;
474 _contentEncoding = NSUTF32LittleEndianStringEncoding;
477 } else if (BytesEqual(bytes, 0xEF, 0xBB, 0xBF)) {
478 _contentEncoding = NSUTF8StringEncoding;
480 } else if (BytesEqual(bytes, 0xFE, 0xFF)) {
481 _contentEncoding = NSUTF16BigEndianStringEncoding;
483 } else if (BytesEqual(bytes, 0x00, 0x00, 0xFE, 0xFF)) {
484 _contentEncoding = NSUTF32BigEndianStringEncoding;
488 _completedByteOrderMarkCheck = YES;
491 - (void)checkForXMLDeclaration {
492 // WebKit interprets the byte order mark (BOM) as the truth and ignores and
493 // subsequent encodings. Thus if a BOM has already been found, there is no
494 // need to check for an XML declaration.
496 _completedXMLDeclarationCheck = YES;
498 // Bail if we have completed this.
499 if (_completedXMLDeclarationCheck)
502 // Do we already know the encoding?
503 if (_contentEncoding) {
504 _completedXMLDeclarationCheck = YES;
508 // Bail if there is not yet enough data to check.
509 if (_pendingDataLength < kMinimumBytesNeededForXMLDecl)
512 // It's possible that the server returned data in small chunks, so coalesce
513 // the data into one buffer if needed.
514 [self coalesceDataIfNeeded:kMinimumBytesNeededForXMLDecl];
516 // Get some data to investigate.
517 NSData* firstData = [_pendingData firstObject];
518 if (!firstData && [firstData length] < kMinimumBytesNeededForXMLDecl) {
523 // Do the same check that WebKit does for an XML declaration. The XML spec
524 // is not exactly clear about what, if anything, can appear before an XML
525 // declaration. Can there be white space? Can there be comments? WebKit only
526 // accepts XML declarations if they are right at the beginning of the content.
527 const uint8* bytes = reinterpret_cast<const uint8*>([firstData bytes]);
528 if (BytesEqual(bytes, '<', '?', 'x', 'm', 'l')) {
529 _contentEncoding = NSISOLatin1StringEncoding;
530 } else if (BytesEqual(bytes, '<', 0, '?', 0, 'x', 0)) {
531 _contentEncoding = NSUTF16LittleEndianStringEncoding;
532 } else if (BytesEqual(bytes, 0, '<', 0, '?', 0, 'x')) {
533 _contentEncoding = NSUTF16BigEndianStringEncoding;
534 } else if (BytesEqual(bytes, '<', 0, 0, 0, '?', 0, 0, 0)) {
535 _contentEncoding = NSUTF32LittleEndianStringEncoding;
536 } else if (BytesEqual(bytes, 0, 0, 0, '<', 0, 0, 0, '?')) {
537 _contentEncoding = NSUTF32BigEndianStringEncoding;
540 _completedXMLDeclarationCheck = YES;
543 - (void)checkIfByteLimitPassed:(const WebCore::CharacterProvider&)provider {
544 // Put a hard limit on the number of characters we check before doing
546 if (provider.bytesProvided() >= kLimitOfBytesToCheckForHeadTag)
547 _completedCheckForWhereToInject = YES;
550 - (void)checkForWhereToInject {
551 // Bail if the check has already been completed.
552 if (_completedCheckForWhereToInject)
555 // Bail if there is not yet enough data to check. We need a complete "<html "
556 // or a complete "<html>"
557 if (_pendingDataLength < kMinimumBytesNeededForHTMLTag)
560 // It's possible that the server returned data in small chunks, so coalesce
561 // the data into one buffer if needed. Try pooling everything into one chunk
562 // that meets the limit of what we look at.
563 [self coalesceDataIfNeeded:kLimitOfBytesToCheckForHeadTag];
565 // Get some data to investigate. Of course we need at least
566 // kMinimumBytesNeededForHTMLTag bytes to do the investigation.
567 NSData* firstData = [_pendingData firstObject];
568 DCHECK([firstData length] >= kMinimumBytesNeededForHTMLTag);
570 const uint8* bytes8 = reinterpret_cast<const uint8*>([firstData bytes]);
572 WebCore::CharacterProvider provider;
573 switch (_contentEncoding) {
574 case NSUTF16BigEndianStringEncoding:
575 case NSUTF16LittleEndianStringEncoding:
576 case NSUTF32BigEndianStringEncoding:
577 case NSUTF32LittleEndianStringEncoding: {
578 const uint16* bytes16 =
579 reinterpret_cast<const uint16*>(bytes8 + _headerLength);
581 provider.setContents(bytes16, [firstData length] - _headerLength);
583 if (_contentEncoding == NSUTF16LittleEndianStringEncoding ||
584 _contentEncoding == NSUTF32LittleEndianStringEncoding)
585 provider.setLittleEndian();
590 provider.setContents(bytes8 + _headerLength,
591 [firstData length] - _headerLength);
596 WebCore::HTMLTokenizer tokenizer;
597 WebCore::HTMLToken token;
598 while(!_completedCheckForWhereToInject &&
599 tokenizer.nextToken(provider, token)) {
600 WebCore::HTMLToken::Type tokenType = token.type();
602 case WebCore::HTMLToken::StartTag: {
603 DEFINE_STATIC_LOCAL_STRING(htmlTag, "html");
605 // If any start tag is seen, the check is complete.
606 _proceedWithInjection = YES;
607 _completedCheckForWhereToInject = YES;
609 // We always have to account for any BOM header when injecting.
610 _byteOffsetAtWhichToInject = _headerLength;
612 if (token.nameEquals(htmlTag, htmlTagLength)) {
613 // There is an html tag, so the insertion needs
614 // to happen after this start tag.
615 _byteOffsetAtWhichToInject += provider.bytesProvided();
622 [self checkIfByteLimitPassed:provider];
629 // There is an early exit case right at the end of the start-tag from the
630 // WebCore::HTMLTokenizer::nextToken(), so double check to see if we hit
632 [self checkIfByteLimitPassed:provider];