2 File: CanonicalRequest.m
3 Abstract: A function for creating canonical HTTP/HTTPS requests.
6 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple
7 Inc. ("Apple") in consideration of your agreement to the following
8 terms, and your use, installation, modification or redistribution of
9 this Apple software constitutes acceptance of these terms. If you do
10 not agree with these terms, please do not use, install, modify or
11 redistribute this Apple software.
13 In consideration of your agreement to abide by the following terms, and
14 subject to these terms, Apple grants you a personal, non-exclusive
15 license, under Apple's copyrights in this original Apple software (the
16 "Apple Software"), to use, reproduce, modify and redistribute the Apple
17 Software, with or without modifications, in source and/or binary forms;
18 provided that if you redistribute the Apple Software in its entirety and
19 without modifications, you must retain this notice and the following
20 text and disclaimers in all such redistributions of the Apple Software.
21 Neither the name, trademarks, service marks or logos of Apple Inc. may
22 be used to endorse or promote products derived from the Apple Software
23 without specific prior written permission from Apple. Except as
24 expressly stated in this notice, no other rights or licenses, express or
25 implied, are granted by Apple herein, including but not limited to any
26 patent rights that may be infringed by your derivative works or by other
27 works in which the Apple Software may be incorporated.
29 The Apple Software is provided by Apple on an "AS IS" basis. APPLE
30 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
31 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
32 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
33 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
35 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
36 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
37 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
38 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
39 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
40 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
41 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
42 POSSIBILITY OF SUCH DAMAGE.
44 Copyright (C) 2014 Apple Inc. All Rights Reserved.
48 #import "CanonicalRequest.h"
52 #pragma mark * URL canonicalization steps
54 /*! A step in the canonicalisation process.
55 * \details The canonicalisation process is made up of a sequence of steps, each of which is
56 * implemented by a function that matches this function pointer. The function gets a URL
57 * and a mutable buffer holding that URL as bytes. The function can mutate the buffer as it
58 * sees fit. It typically does this by calling CFURLGetByteRangeForComponent to find the range
59 * of interest in the buffer. In that case bytesInserted is the amount to adjust that range,
60 * and the function should modify that to account for any bytes it inserts or deletes. If
61 * the function modifies the buffer too much, it can return kCFNotFound to force the system
62 * to re-create the URL from the buffer.
63 * \param url The original URL to work on.
64 * \param urlData The URL as a mutable buffer; the routine modifies this.
65 * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
66 * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
69 typedef CFIndex (*CanonicalRequestStepFunction)(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted);
71 /*! The post-scheme separate should be "://"; if that's not the case, fix it.
72 * \param url The original URL to work on.
73 * \param urlData The URL as a mutable buffer; the routine modifies this.
74 * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
75 * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
78 static CFIndex FixPostSchemeSeparator(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
81 uint8_t * urlDataBytes;
82 NSUInteger urlDataLength;
84 NSUInteger separatorLength;
85 NSUInteger expectedSeparatorLength;
88 assert(urlData != nil);
89 assert(bytesInserted >= 0);
91 range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentScheme, NULL);
92 if (range.location != kCFNotFound) {
93 assert(range.location >= 0);
94 assert(range.length >= 0);
96 urlDataBytes = [urlData mutableBytes];
97 urlDataLength = [urlData length];
100 cursor = (NSUInteger) range.location + (NSUInteger) bytesInserted + (NSUInteger) range.length;
101 if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == ':') ) {
103 separatorLength += 1;
104 if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == '/') ) {
106 separatorLength += 1;
107 if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == '/') ) {
109 separatorLength += 1;
113 #pragma unused(cursor) // quietens an analyser warning
115 expectedSeparatorLength = strlen("://");
116 if (separatorLength != expectedSeparatorLength) {
117 [urlData replaceBytesInRange:NSMakeRange((NSUInteger) range.location + (NSUInteger) bytesInserted + (NSUInteger) range.length, separatorLength) withBytes:"://" length:expectedSeparatorLength];
118 bytesInserted = kCFNotFound; // have to build everything now
122 return bytesInserted;
125 /*! The scheme should be lower case; if it's not, make it so.
126 * \param url The original URL to work on.
127 * \param urlData The URL as a mutable buffer; the routine modifies this.
128 * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
129 * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
132 static CFIndex LowercaseScheme(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
135 uint8_t * urlDataBytes;
139 assert(urlData != nil);
140 assert(bytesInserted >= 0);
142 range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentScheme, NULL);
143 if (range.location != kCFNotFound) {
144 assert(range.location >= 0);
145 assert(range.length >= 0);
147 urlDataBytes = [urlData mutableBytes];
148 for (i = range.location + bytesInserted; i < (range.location + bytesInserted + range.length); i++) {
149 urlDataBytes[i] = (uint8_t) tolower_l(urlDataBytes[i], NULL);
152 return bytesInserted;
155 /*! The host should be lower case; if it's not, make it so.
156 * \param url The original URL to work on.
157 * \param urlData The URL as a mutable buffer; the routine modifies this.
158 * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
159 * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
162 static CFIndex LowercaseHost(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
163 // The host should be lower case; if it's not, make it so.
166 uint8_t * urlDataBytes;
170 assert(urlData != nil);
171 assert(bytesInserted >= 0);
173 range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentHost, NULL);
174 if (range.location != kCFNotFound) {
175 assert(range.location >= 0);
176 assert(range.length >= 0);
178 urlDataBytes = [urlData mutableBytes];
179 for (i = range.location + bytesInserted; i < (range.location + bytesInserted + range.length); i++) {
180 urlDataBytes[i] = (uint8_t) tolower_l(urlDataBytes[i], NULL);
183 return bytesInserted;
186 /*! An empty host should be treated as "localhost" case; if it's not, make it so.
187 * \param url The original URL to work on.
188 * \param urlData The URL as a mutable buffer; the routine modifies this.
189 * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
190 * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
193 static CFIndex FixEmptyHost(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
196 CFRange rangeWithSeparator;
199 assert(urlData != nil);
200 assert(bytesInserted >= 0);
202 range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentHost, &rangeWithSeparator);
203 if (range.length == 0) {
204 NSUInteger localhostLength;
206 assert(range.location >= 0);
207 assert(range.length >= 0);
209 localhostLength = strlen("localhost");
210 if (range.location != kCFNotFound) {
211 [urlData replaceBytesInRange:NSMakeRange( (NSUInteger) range.location + (NSUInteger) bytesInserted, 0) withBytes:"localhost" length:localhostLength];
212 bytesInserted += localhostLength;
213 } else if ( (rangeWithSeparator.location != kCFNotFound) && (rangeWithSeparator.length == 0) ) {
214 [urlData replaceBytesInRange:NSMakeRange((NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"localhost" length:localhostLength];
215 bytesInserted += localhostLength;
218 return bytesInserted;
221 /*! Transform an empty URL path to "/". For example, "http://www.apple.com" becomes "http://www.apple.com/".
222 * \param url The original URL to work on.
223 * \param urlData The URL as a mutable buffer; the routine modifies this.
224 * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
225 * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
228 static CFIndex FixEmptyPath(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
231 CFRange rangeWithSeparator;
234 assert(urlData != nil);
235 assert(bytesInserted >= 0);
237 range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPath, &rangeWithSeparator);
238 // The following is not a typo. We use rangeWithSeparator to find where to insert the
239 // "/" and the range length to decide whether we /need/ to insert the "/".
240 if ( (rangeWithSeparator.location != kCFNotFound) && (range.length == 0) ) {
241 assert(range.location >= 0);
242 assert(range.length >= 0);
243 assert(rangeWithSeparator.location >= 0);
244 assert(rangeWithSeparator.length >= 0);
246 [urlData replaceBytesInRange:NSMakeRange( (NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"/" length:1];
249 return bytesInserted;
252 /*! If the user specified the default port (80 for HTTP, 443 for HTTPS), remove it from the URL.
253 * \details Actually this code is disabled because the equivalent code in the default protocol
254 * handler has also been disabled; some setups depend on get the port number in the URL, even if it
256 * \param url The original URL to work on.
257 * \param urlData The URL as a mutable buffer; the routine modifies this.
258 * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
259 * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
262 __attribute__((unused)) static CFIndex DeleteDefaultPort(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
268 uint8_t * urlDataBytes;
269 NSString * portNumberStr;
273 assert(urlData != nil);
274 assert(bytesInserted >= 0);
276 scheme = [[url scheme] lowercaseString];
277 assert(scheme != nil);
279 isHTTP = [scheme isEqual:@"http" ];
280 isHTTPS = [scheme isEqual:@"https"];
282 range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPort, NULL);
283 if (range.location != kCFNotFound) {
284 assert(range.location >= 0);
285 assert(range.length >= 0);
287 urlDataBytes = [urlData mutableBytes];
289 portNumberStr = [[NSString alloc] initWithBytes:&urlDataBytes[range.location + bytesInserted] length:(NSUInteger) range.length encoding:NSUTF8StringEncoding];
290 if (portNumberStr != nil) {
291 portNumber = [portNumberStr intValue];
292 if ( (isHTTP && (portNumber == 80)) || (isHTTPS && (portNumber == 443)) ) {
293 // -1 and +1 to account for the leading ":"
294 [urlData replaceBytesInRange:NSMakeRange((NSUInteger) range.location + (NSUInteger) bytesInserted - 1, (NSUInteger) range.length + 1) withBytes:NULL length:0];
295 bytesInserted -= (range.length + 1);
299 return bytesInserted;
302 #pragma mark * Other request canonicalization
304 /*! Canonicalise the request headers.
305 * \param request The request to canonicalise.
308 static void CanonicaliseHeaders(NSMutableURLRequest * request)
310 // If there's no content type and the request is a POST with a body, add a default
311 // content type of "application/x-www-form-urlencoded".
313 if ( ([request valueForHTTPHeaderField:@"Content-Type"] == nil)
314 && ([[request HTTPMethod] caseInsensitiveCompare:@"POST"] == NSOrderedSame)
315 && (([request HTTPBody] != nil) || ([request HTTPBodyStream] != nil)) ) {
316 [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
319 // If there's no "Accept" header, add a default.
321 if ([request valueForHTTPHeaderField:@"Accept"] == nil) {
322 [request setValue:@"*/*" forHTTPHeaderField:@"Accept"];
325 // If there's not "Accept-Encoding" header, add a default.
327 if ([request valueForHTTPHeaderField:@"Accept-Encoding"] == nil) {
328 [request setValue:@"gzip, deflate" forHTTPHeaderField:@"Accept-Encoding"];
331 // If there's not an "Accept-Language" headre, add a default. This is quite bogus; ideally we
332 // should derive the correct "Accept-Language" value from the langauge that the app is running
333 // in. However, that's quite difficult to get right, so rather than show some general purpose
334 // code that might fail in some circumstances, I've decided to just hardwire US English.
335 // If you use this code in your own app you can customise it as you see fit. One option might be
336 // to base this value on -[NSBundle preferredLocalizations], so that the web page comes back in
337 // the language that the app is running in.
339 if ([request valueForHTTPHeaderField:@"Accept-Language"] == nil) {
340 [request setValue:@"en-us" forHTTPHeaderField:@"Accept-Language"];
346 extern NSMutableURLRequest * CanonicalRequestForRequest(NSURLRequest *request)
348 NSMutableURLRequest * result;
351 assert(request != nil);
353 // Make a mutable copy of the request.
355 result = [request mutableCopy];
357 // First up check that we're dealing with HTTP or HTTPS. If not, do nothing (why were we
360 scheme = [[[request URL] scheme] lowercaseString];
361 assert(scheme != nil);
363 if ( ! [scheme isEqual:@"http" ] && ! [scheme isEqual:@"https"]) {
366 CFIndex bytesInserted;
368 NSMutableData * urlData;
369 static const CanonicalRequestStepFunction kStepFunctions[] = {
370 FixPostSchemeSeparator,
374 // DeleteDefaultPort, -- The built-in canonicalizer has stopped doing this, so we don't do it either.
380 // Canonicalise the URL by executing each of our step functions.
382 bytesInserted = kCFNotFound;
384 requestURL = [request URL];
385 assert(requestURL != nil);
387 stepCount = sizeof(kStepFunctions) / sizeof(*kStepFunctions);
388 for (stepIndex = 0; stepIndex < stepCount; stepIndex++) {
390 // If we don't have valid URL data, create it from the URL.
392 assert(requestURL != nil);
393 if (bytesInserted == kCFNotFound) {
394 NSData * urlDataImmutable;
396 urlDataImmutable = CFBridgingRelease( CFURLCreateData(NULL, (CFURLRef) requestURL, kCFStringEncodingUTF8, true) );
397 assert(urlDataImmutable != nil);
399 urlData = [urlDataImmutable mutableCopy];
400 assert(urlData != nil);
404 assert(urlData != nil);
408 bytesInserted = kStepFunctions[stepIndex](requestURL, urlData, bytesInserted);
410 // Note: The following logging is useful when debugging this code. Change the
411 // if expression to YES to enable it.
414 fprintf(stderr, " [%zu] %.*s\n", stepIndex, (int) [urlData length], (const char *) [urlData bytes]);
417 // If the step invalidated our URL (or we're on the last step, whereupon we'll need
418 // the URL outside of the loop), recreate the URL from the URL data.
420 if ( (bytesInserted == kCFNotFound) || ((stepIndex + 1) == stepCount) ) {
421 requestURL = CFBridgingRelease( CFURLCreateWithBytes(NULL, [urlData bytes], (CFIndex) [urlData length], kCFStringEncodingUTF8, NULL) );
422 assert(requestURL != nil);
428 [result setURL:requestURL];
430 // Canonicalise the headers.
432 CanonicaliseHeaders(result);