OSDN Git Service

UIを調整
[kcd/KCD.git] / KCD / CanonicalRequest.m
1 /*
2      File: CanonicalRequest.m
3  Abstract: A function for creating canonical HTTP/HTTPS requests.
4   Version: 1.1
5  
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.
12  
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.
28  
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.
34  
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.
43  
44  Copyright (C) 2014 Apple Inc. All Rights Reserved.
45  
46  */
47
48 #import "CanonicalRequest.h"
49
50 #include <xlocale.h>
51
52 #pragma mark * URL canonicalization steps 
53
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.
67  */
68
69 typedef CFIndex (*CanonicalRequestStepFunction)(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted);
70
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.
76  */
77
78 static CFIndex FixPostSchemeSeparator(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
79 {
80     CFRange     range;
81     uint8_t *   urlDataBytes;
82     NSUInteger  urlDataLength;
83     NSUInteger  cursor;
84     NSUInteger  separatorLength;
85     NSUInteger  expectedSeparatorLength;
86     
87     assert(url != nil);
88     assert(urlData != nil);
89     assert(bytesInserted >= 0);
90
91     range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentScheme, NULL);
92     if (range.location != kCFNotFound) {
93         assert(range.location >= 0);
94         assert(range.length >= 0);
95         
96         urlDataBytes  = [urlData mutableBytes];
97         urlDataLength = [urlData length];
98         
99         separatorLength = 0;
100         cursor = (NSUInteger) range.location + (NSUInteger) bytesInserted + (NSUInteger) range.length;
101         if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == ':') ) {
102             cursor += 1;
103             separatorLength += 1;
104             if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == '/') ) {
105                 cursor += 1;
106                 separatorLength += 1;
107                 if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == '/') ) {
108                     cursor += 1;
109                     separatorLength += 1;
110                 }
111             }
112         }
113         #pragma unused(cursor)          // quietens an analyser warning
114                 
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
119         }
120     }
121     
122     return bytesInserted;
123 }
124
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.
130  */
131
132 static CFIndex LowercaseScheme(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
133 {
134     CFRange     range;
135     uint8_t *   urlDataBytes;
136     CFIndex     i;
137     
138     assert(url != nil);
139     assert(urlData != nil);
140     assert(bytesInserted >= 0);
141
142     range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentScheme, NULL);
143     if (range.location != kCFNotFound) {
144         assert(range.location >= 0);
145         assert(range.length >= 0);
146
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);
150         }
151     }
152     return bytesInserted;
153 }
154
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.
160  */
161
162 static CFIndex LowercaseHost(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
163     // The host should be lower case; if it's not, make it so.
164 {
165     CFRange     range;
166     uint8_t *   urlDataBytes;
167     CFIndex     i;
168     
169     assert(url != nil);
170     assert(urlData != nil);
171     assert(bytesInserted >= 0);
172
173     range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentHost, NULL);
174     if (range.location != kCFNotFound) {
175         assert(range.location >= 0);
176         assert(range.length >= 0);
177
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);
181         }
182     }
183     return bytesInserted;
184 }
185
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.
191  */
192
193 static CFIndex FixEmptyHost(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
194 {
195     CFRange     range;
196     CFRange     rangeWithSeparator;
197     
198     assert(url != nil);
199     assert(urlData != nil);
200     assert(bytesInserted >= 0);
201
202     range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentHost, &rangeWithSeparator);
203     if (range.length == 0) {
204         NSUInteger  localhostLength;
205
206         assert(range.location >= 0);
207         assert(range.length >= 0);
208         
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;
216         }
217     }
218     return bytesInserted;
219 }
220
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.
226  */
227
228 static CFIndex FixEmptyPath(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
229 {
230     CFRange     range;
231     CFRange     rangeWithSeparator;
232     
233     assert(url != nil);
234     assert(urlData != nil);
235     assert(bytesInserted >= 0);
236
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);
245
246         [urlData replaceBytesInRange:NSMakeRange( (NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"/" length:1];
247         bytesInserted += 1;
248     }
249     return bytesInserted;
250 }
251
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 
255  *  is the default.
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.
260  */
261
262 __attribute__((unused)) static CFIndex DeleteDefaultPort(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted) 
263 {
264     NSString *  scheme;
265     BOOL        isHTTP;
266     BOOL        isHTTPS;
267     CFRange     range;
268     uint8_t *   urlDataBytes;
269     NSString *  portNumberStr;
270     int         portNumber;
271
272     assert(url != nil);
273     assert(urlData != nil);
274     assert(bytesInserted >= 0);
275
276     scheme = [[url scheme] lowercaseString];
277     assert(scheme != nil);
278     
279     isHTTP  = [scheme isEqual:@"http" ];
280     isHTTPS = [scheme isEqual:@"https"];
281     
282     range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPort, NULL);
283     if (range.location != kCFNotFound) {
284         assert(range.location >= 0);
285         assert(range.length >= 0);
286
287         urlDataBytes = [urlData mutableBytes];
288         
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);
296             }
297         }
298     }
299     return bytesInserted;
300 }
301
302 #pragma mark * Other request canonicalization
303
304 /*! Canonicalise the request headers.
305  *  \param request The request to canonicalise.
306  */
307
308 static void CanonicaliseHeaders(NSMutableURLRequest * request)
309 {
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".
312     
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"];
317     }
318     
319     // If there's no "Accept" header, add a default.
320     
321     if ([request valueForHTTPHeaderField:@"Accept"] == nil) {
322         [request setValue:@"*/*" forHTTPHeaderField:@"Accept"];
323     }
324
325     // If there's not "Accept-Encoding" header, add a default.
326     
327     if ([request valueForHTTPHeaderField:@"Accept-Encoding"] == nil) {
328         [request setValue:@"gzip, deflate" forHTTPHeaderField:@"Accept-Encoding"];
329     }
330
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.
338     
339     if ([request valueForHTTPHeaderField:@"Accept-Language"] == nil) {
340         [request setValue:@"en-us" forHTTPHeaderField:@"Accept-Language"];
341     }
342 }
343
344 #pragma mark * API
345
346 extern NSMutableURLRequest * CanonicalRequestForRequest(NSURLRequest *request)
347 {
348     NSMutableURLRequest *   result;
349     NSString *              scheme;
350
351     assert(request != nil);
352
353     // Make a mutable copy of the request.
354     
355     result = [request mutableCopy];
356     
357     // First up check that we're dealing with HTTP or HTTPS.  If not, do nothing (why were we 
358     // we even called?).
359     
360     scheme = [[[request URL] scheme] lowercaseString];
361     assert(scheme != nil);
362     
363     if ( ! [scheme isEqual:@"http" ] && ! [scheme isEqual:@"https"]) {
364         assert(NO);
365     } else {
366         CFIndex         bytesInserted;
367         NSURL *         requestURL;
368         NSMutableData * urlData;
369         static const CanonicalRequestStepFunction kStepFunctions[] = {
370             FixPostSchemeSeparator, 
371             LowercaseScheme, 
372             LowercaseHost, 
373             FixEmptyHost, 
374             // DeleteDefaultPort,       -- The built-in canonicalizer has stopped doing this, so we don't do it either.
375             FixEmptyPath
376         };
377         size_t          stepIndex;
378         size_t          stepCount;
379         
380         // Canonicalise the URL by executing each of our step functions.
381         
382         bytesInserted = kCFNotFound;
383         urlData = nil;
384         requestURL = [request URL];
385         assert(requestURL != nil);
386
387         stepCount = sizeof(kStepFunctions) / sizeof(*kStepFunctions);
388         for (stepIndex = 0; stepIndex < stepCount; stepIndex++) {
389         
390             // If we don't have valid URL data, create it from the URL.
391             
392             assert(requestURL != nil);
393             if (bytesInserted == kCFNotFound) {
394                 NSData *    urlDataImmutable;
395
396                 urlDataImmutable = CFBridgingRelease( CFURLCreateData(NULL, (CFURLRef) requestURL, kCFStringEncodingUTF8, true) );
397                 assert(urlDataImmutable != nil);
398                 
399                 urlData = [urlDataImmutable mutableCopy];
400                 assert(urlData != nil);
401                 
402                 bytesInserted = 0;
403             }
404             assert(urlData != nil);
405             
406             // Run the step.
407             
408             bytesInserted = kStepFunctions[stepIndex](requestURL, urlData, bytesInserted);
409             
410             // Note: The following logging is useful when debugging this code.  Change the 
411             // if expression to YES to enable it.
412             
413             if (NO) {
414                 fprintf(stderr, "  [%zu] %.*s\n", stepIndex, (int) [urlData length], (const char *) [urlData bytes]);
415             }
416             
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.
419             
420             if ( (bytesInserted == kCFNotFound) || ((stepIndex + 1) == stepCount) ) {
421                 requestURL = CFBridgingRelease( CFURLCreateWithBytes(NULL, [urlData bytes], (CFIndex) [urlData length], kCFStringEncodingUTF8, NULL) );
422                 assert(requestURL != nil);
423                 
424                 urlData = nil;
425             }
426         }
427
428         [result setURL:requestURL];
429         
430         // Canonicalise the headers.
431         
432         CanonicaliseHeaders(result);
433     }
434     
435     return result;
436 }