github.com/psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPCanonicalRequest.m (about) 1 /* 2 File: JAHPCanonicalRequest.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 "JAHPCanonicalRequest.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 /* Note: 207 * We have updated this Apple provided (and JAHPAuthenticatingHTTPProtocol subsumed) code 208 * to fix a bug in this function inherited from the original source. The comments below 209 * detail the fix. 210 * 211 * Source: https://developer.apple.com/library/content/samplecode/CustomHTTPProtocol/Listings/CustomHTTPProtocol_Core_Code_CanonicalRequest_m.html 212 * 213 * Fix: 214 * Removed `assert(range.location >= 0)` which fails every time because 215 * if there is an empty host then range.location == kCFNotFound == -1. 216 * Fix: 217 * Added rangeWithSeperator assertion. Length should always be non-negative. 218 * 219 * Please refer to the comments in CFURL.h for CFURLGetByteRangeForComponent:. 220 * An excerpt: 221 * If non-NULL, rangeIncludingSeparators gives the range of component 222 * including the sequences that separate component from the previous and 223 * next components. If there is no previous or next component, that end of 224 * rangeIncludingSeparators will match the range of the component itself. 225 * If url does not contain the given component type, (kCFNotFound, 0) is 226 * returned, and rangeIncludingSeparators is set to the location where the 227 * component would be inserted. 228 */ 229 assert(range.length >= 0); 230 assert(rangeWithSeparator.length >= 0); 231 232 localhostLength = strlen("localhost"); 233 if (range.location != kCFNotFound) { 234 [urlData replaceBytesInRange:NSMakeRange( (NSUInteger) range.location + (NSUInteger) bytesInserted, 0) withBytes:"localhost" length:localhostLength]; 235 bytesInserted += localhostLength; 236 } else if ( (rangeWithSeparator.location != kCFNotFound) && (rangeWithSeparator.length == 0) ) { 237 [urlData replaceBytesInRange:NSMakeRange((NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"localhost" length:localhostLength]; 238 bytesInserted += localhostLength; 239 } 240 } 241 return bytesInserted; 242 } 243 244 /*! Transform an empty URL path to "/". For example, "http://www.apple.com" becomes "http://www.apple.com/". 245 * \param url The original URL to work on. 246 * \param urlData The URL as a mutable buffer; the routine modifies this. 247 * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer. 248 * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed. 249 */ 250 251 static CFIndex FixEmptyPath(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted) 252 { 253 CFRange range; 254 CFRange rangeWithSeparator; 255 256 assert(url != nil); 257 assert(urlData != nil); 258 assert(bytesInserted >= 0); 259 260 range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPath, &rangeWithSeparator); 261 // The following is not a typo. We use rangeWithSeparator to find where to insert the 262 // "/" and the range length to decide whether we /need/ to insert the "/". 263 if ( (rangeWithSeparator.location != kCFNotFound) && (range.length == 0) ) { 264 assert(range.location >= 0); 265 assert(range.length >= 0); 266 assert(rangeWithSeparator.location >= 0); 267 assert(rangeWithSeparator.length >= 0); 268 269 [urlData replaceBytesInRange:NSMakeRange( (NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"/" length:1]; 270 bytesInserted += 1; 271 } 272 return bytesInserted; 273 } 274 275 /*! If the user specified the default port (80 for HTTP, 443 for HTTPS), remove it from the URL. 276 * \details Actually this code is disabled because the equivalent code in the default protocol 277 * handler has also been disabled; some setups depend on get the port number in the URL, even if it 278 * is the default. 279 * \param url The original URL to work on. 280 * \param urlData The URL as a mutable buffer; the routine modifies this. 281 * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer. 282 * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed. 283 */ 284 285 __attribute__((unused)) static CFIndex DeleteDefaultPort(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted) 286 { 287 NSString * scheme; 288 BOOL isHTTP; 289 BOOL isHTTPS; 290 CFRange range; 291 uint8_t * urlDataBytes; 292 NSString * portNumberStr; 293 int portNumber; 294 295 assert(url != nil); 296 assert(urlData != nil); 297 assert(bytesInserted >= 0); 298 299 scheme = [[url scheme] lowercaseString]; 300 assert(scheme != nil); 301 302 isHTTP = [scheme isEqual:@"http" ]; 303 isHTTPS = [scheme isEqual:@"https"]; 304 305 range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPort, NULL); 306 if (range.location != kCFNotFound) { 307 assert(range.location >= 0); 308 assert(range.length >= 0); 309 310 urlDataBytes = [urlData mutableBytes]; 311 312 portNumberStr = [[NSString alloc] initWithBytes:&urlDataBytes[range.location + bytesInserted] length:(NSUInteger) range.length encoding:NSUTF8StringEncoding]; 313 if (portNumberStr != nil) { 314 portNumber = [portNumberStr intValue]; 315 if ( (isHTTP && (portNumber == 80)) || (isHTTPS && (portNumber == 443)) ) { 316 // -1 and +1 to account for the leading ":" 317 [urlData replaceBytesInRange:NSMakeRange((NSUInteger) range.location + (NSUInteger) bytesInserted - 1, (NSUInteger) range.length + 1) withBytes:NULL length:0]; 318 bytesInserted -= (range.length + 1); 319 } 320 } 321 } 322 return bytesInserted; 323 } 324 325 #pragma mark * Other request canonicalization 326 327 /*! Canonicalise the request headers. 328 * \param request The request to canonicalise. 329 */ 330 331 static void CanonicaliseHeaders(NSMutableURLRequest * request) 332 { 333 // If there's no content type and the request is a POST with a body, add a default 334 // content type of "application/x-www-form-urlencoded". 335 336 if ( ([request valueForHTTPHeaderField:@"Content-Type"] == nil) 337 && ([[request HTTPMethod] caseInsensitiveCompare:@"POST"] == NSOrderedSame) 338 && (([request HTTPBody] != nil) || ([request HTTPBodyStream] != nil)) ) { 339 [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; 340 } 341 342 // If there's no "Accept" header, add a default. 343 344 if ([request valueForHTTPHeaderField:@"Accept"] == nil) { 345 [request setValue:@"*/*" forHTTPHeaderField:@"Accept"]; 346 } 347 348 // If there's not "Accept-Encoding" header, add a default. 349 350 if ([request valueForHTTPHeaderField:@"Accept-Encoding"] == nil) { 351 [request setValue:@"gzip, deflate" forHTTPHeaderField:@"Accept-Encoding"]; 352 } 353 354 // If there's not an "Accept-Language" headre, add a default. This is quite bogus; ideally we 355 // should derive the correct "Accept-Language" value from the langauge that the app is running 356 // in. However, that's quite difficult to get right, so rather than show some general purpose 357 // code that might fail in some circumstances, I've decided to just hardwire US English. 358 // If you use this code in your own app you can customise it as you see fit. One option might be 359 // to base this value on -[NSBundle preferredLocalizations], so that the web page comes back in 360 // the language that the app is running in. 361 362 if ([request valueForHTTPHeaderField:@"Accept-Language"] == nil) { 363 [request setValue:@"en-us" forHTTPHeaderField:@"Accept-Language"]; 364 } 365 } 366 367 #pragma mark * API 368 369 extern NSMutableURLRequest * JAHPCanonicalRequestForRequest(NSURLRequest *request) 370 { 371 NSMutableURLRequest * result; 372 NSString * scheme; 373 374 assert(request != nil); 375 376 // Make a mutable copy of the request. 377 378 result = [request mutableCopy]; 379 380 // First up check that we're dealing with HTTP or HTTPS. If not, do nothing (why were we 381 // we even called?). 382 383 scheme = [[[request URL] scheme] lowercaseString]; 384 assert(scheme != nil); 385 386 if ( ! [scheme isEqual:@"http" ] && ! [scheme isEqual:@"https"]) { 387 assert(NO); 388 } else { 389 CFIndex bytesInserted; 390 NSURL * requestURL; 391 NSMutableData * urlData; 392 static const CanonicalRequestStepFunction kStepFunctions[] = { 393 FixPostSchemeSeparator, 394 LowercaseScheme, 395 LowercaseHost, 396 FixEmptyHost, 397 // DeleteDefaultPort, -- The built-in canonicalizer has stopped doing this, so we don't do it either. 398 FixEmptyPath 399 }; 400 size_t stepIndex; 401 size_t stepCount; 402 403 // Canonicalise the URL by executing each of our step functions. 404 405 bytesInserted = kCFNotFound; 406 urlData = nil; 407 requestURL = [request URL]; 408 assert(requestURL != nil); 409 410 stepCount = sizeof(kStepFunctions) / sizeof(*kStepFunctions); 411 for (stepIndex = 0; stepIndex < stepCount; stepIndex++) { 412 413 // If we don't have valid URL data, create it from the URL. 414 415 assert(requestURL != nil); 416 if (bytesInserted == kCFNotFound) { 417 NSData * urlDataImmutable; 418 419 urlDataImmutable = CFBridgingRelease( CFURLCreateData(NULL, (CFURLRef) requestURL, kCFStringEncodingUTF8, true) ); 420 assert(urlDataImmutable != nil); 421 422 urlData = [urlDataImmutable mutableCopy]; 423 assert(urlData != nil); 424 425 bytesInserted = 0; 426 } 427 assert(urlData != nil); 428 429 // Run the step. 430 431 bytesInserted = kStepFunctions[stepIndex](requestURL, urlData, bytesInserted); 432 433 // Note: The following logging is useful when debugging this code. Change the 434 // if expression to YES to enable it. 435 436 if (/* DISABLES CODE */ (NO)) { 437 fprintf(stderr, " [%zu] %.*s\n", stepIndex, (int) [urlData length], (const char *) [urlData bytes]); 438 } 439 440 // If the step invalidated our URL (or we're on the last step, whereupon we'll need 441 // the URL outside of the loop), recreate the URL from the URL data. 442 443 if ( (bytesInserted == kCFNotFound) || ((stepIndex + 1) == stepCount) ) { 444 requestURL = CFBridgingRelease( CFURLCreateWithBytes(NULL, [urlData bytes], (CFIndex) [urlData length], kCFStringEncodingUTF8, NULL) ); 445 assert(requestURL != nil); 446 447 urlData = nil; 448 } 449 } 450 451 [result setURL:requestURL]; 452 453 // Canonicalise the headers. 454 455 CanonicaliseHeaders(result); 456 } 457 458 return result; 459 }