// // Example // man // // Created by man 11/11/2018. // Copyright © 2020 man. All rights reserved. // #import "_CanonicalRequest.h" #include #pragma mark * URL canonicalization steps /*! A step in the canonicalisation process. * \details The canonicalisation process is made up of a sequence of steps, each of which is * implemented by a function that matches this function pointer. The function gets a URL * and a mutable buffer holding that URL as bytes. The function can mutate the buffer as it * sees fit. It typically does this by calling CFURLGetByteRangeForComponent to find the range * of interest in the buffer. In that case bytesInserted is the amount to adjust that range, * and the function should modify that to account for any bytes it inserts or deletes. If * the function modifies the buffer too much, it can return kCFNotFound to force the system * to re-create the URL from the buffer. * \param url The original URL to work on. * \param urlData The URL as a mutable buffer; the routine modifies this. * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer. * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed. */ typedef CFIndex (*CanonicalRequestStepFunction)(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted); /*! The post-scheme separate should be "://"; if that's not the case, fix it. * \param url The original URL to work on. * \param urlData The URL as a mutable buffer; the routine modifies this. * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer. * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed. */ static CFIndex FixPostSchemeSeparator(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted) { CFRange range; uint8_t * urlDataBytes; NSUInteger urlDataLength; NSUInteger cursor; NSUInteger separatorLength; NSUInteger expectedSeparatorLength; //assert(url != nil); //assert(urlData != nil); //assert(bytesInserted >= 0); range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentScheme, NULL); if (range.location != kCFNotFound) { //assert(range.location >= 0); //assert(range.length >= 0); urlDataBytes = [urlData mutableBytes]; urlDataLength = [urlData length]; separatorLength = 0; cursor = (NSUInteger) range.location + (NSUInteger) bytesInserted + (NSUInteger) range.length; if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == ':') ) { cursor += 1; separatorLength += 1; if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == '/') ) { cursor += 1; separatorLength += 1; if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == '/') ) { cursor += 1; separatorLength += 1; } } } #pragma unused(cursor) // quietens an analyser warning expectedSeparatorLength = strlen("://"); if (separatorLength != expectedSeparatorLength) { [urlData replaceBytesInRange:NSMakeRange((NSUInteger) range.location + (NSUInteger) bytesInserted + (NSUInteger) range.length, separatorLength) withBytes:"://" length:expectedSeparatorLength]; bytesInserted = kCFNotFound; // have to build everything now } } return bytesInserted; } /*! The scheme should be lower case; if it's not, make it so. * \param url The original URL to work on. * \param urlData The URL as a mutable buffer; the routine modifies this. * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer. * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed. */ static CFIndex LowercaseScheme(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted) { CFRange range; uint8_t * urlDataBytes; CFIndex i; //assert(url != nil); //assert(urlData != nil); //assert(bytesInserted >= 0); range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentScheme, NULL); if (range.location != kCFNotFound) { //assert(range.location >= 0); //assert(range.length >= 0); urlDataBytes = [urlData mutableBytes]; for (i = range.location + bytesInserted; i < (range.location + bytesInserted + range.length); i++) { urlDataBytes[i] = (uint8_t) tolower_l(urlDataBytes[i], NULL); } } return bytesInserted; } /*! The host should be lower case; if it's not, make it so. * \param url The original URL to work on. * \param urlData The URL as a mutable buffer; the routine modifies this. * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer. * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed. */ static CFIndex LowercaseHost(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted) // The host should be lower case; if it's not, make it so. { CFRange range; uint8_t * urlDataBytes; CFIndex i; //assert(url != nil); //assert(urlData != nil); //assert(bytesInserted >= 0); range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentHost, NULL); if (range.location != kCFNotFound) { //assert(range.location >= 0); //assert(range.length >= 0); urlDataBytes = [urlData mutableBytes]; for (i = range.location + bytesInserted; i < (range.location + bytesInserted + range.length); i++) { urlDataBytes[i] = (uint8_t) tolower_l(urlDataBytes[i], NULL); } } return bytesInserted; } /*! An empty host should be treated as "localhost" case; if it's not, make it so. * \param url The original URL to work on. * \param urlData The URL as a mutable buffer; the routine modifies this. * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer. * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed. */ static CFIndex FixEmptyHost(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted) { CFRange range; CFRange rangeWithSeparator; //assert(url != nil); //assert(urlData != nil); //assert(bytesInserted >= 0); range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentHost, &rangeWithSeparator); if (range.length == 0) { NSUInteger localhostLength; //assert(range.location >= 0); //assert(range.length >= 0); localhostLength = strlen("localhost"); if (range.location != kCFNotFound) { [urlData replaceBytesInRange:NSMakeRange( (NSUInteger) range.location + (NSUInteger) bytesInserted, 0) withBytes:"localhost" length:localhostLength]; bytesInserted += localhostLength; } else if ( (rangeWithSeparator.location != kCFNotFound) && (rangeWithSeparator.length == 0) ) { [urlData replaceBytesInRange:NSMakeRange((NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"localhost" length:localhostLength]; bytesInserted += localhostLength; } } return bytesInserted; } /*! Transform an empty URL path to "/". For example, "http://www.apple.com" becomes "http://www.apple.com/". * \param url The original URL to work on. * \param urlData The URL as a mutable buffer; the routine modifies this. * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer. * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed. */ static CFIndex FixEmptyPath(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted) { CFRange range; CFRange rangeWithSeparator; //assert(url != nil); //assert(urlData != nil); //assert(bytesInserted >= 0); range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPath, &rangeWithSeparator); // The following is not a typo. We use rangeWithSeparator to find where to insert the // "/" and the range length to decide whether we /need/ to insert the "/". if ( (rangeWithSeparator.location != kCFNotFound) && (range.length == 0) ) { //assert(range.location >= 0); //assert(range.length >= 0); //assert(rangeWithSeparator.location >= 0); //assert(rangeWithSeparator.length >= 0); [urlData replaceBytesInRange:NSMakeRange( (NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"/" length:1]; bytesInserted += 1; } return bytesInserted; } /*! If the user specified the default port (80 for HTTP, 443 for HTTPS), remove it from the URL. * \details Actually this code is disabled because the equivalent code in the default protocol * handler has also been disabled; some setups depend on get the port number in the URL, even if it * is the default. * \param url The original URL to work on. * \param urlData The URL as a mutable buffer; the routine modifies this. * \param bytesInserted The number of bytes that have been inserted so far the mutable buffer. * \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed. */ __attribute__((unused)) static CFIndex DeleteDefaultPort(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted) { NSString * scheme; BOOL isHTTP; BOOL isHTTPS; CFRange range; uint8_t * urlDataBytes; NSString * portNumberStr; int portNumber; //assert(url != nil); //assert(urlData != nil); //assert(bytesInserted >= 0); scheme = [[url scheme] lowercaseString]; //assert(scheme != nil); isHTTP = [scheme isEqual:@"http" ]; isHTTPS = [scheme isEqual:@"https"]; range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPort, NULL); if (range.location != kCFNotFound) { //assert(range.location >= 0); //assert(range.length >= 0); urlDataBytes = [urlData mutableBytes]; portNumberStr = [[NSString alloc] initWithBytes:&urlDataBytes[range.location + bytesInserted] length:(NSUInteger) range.length encoding:NSUTF8StringEncoding]; if (portNumberStr != nil) { portNumber = [portNumberStr intValue]; if ( (isHTTP && (portNumber == 80)) || (isHTTPS && (portNumber == 443)) ) { // -1 and +1 to account for the leading ":" [urlData replaceBytesInRange:NSMakeRange((NSUInteger) range.location + (NSUInteger) bytesInserted - 1, (NSUInteger) range.length + 1) withBytes:NULL length:0]; bytesInserted -= (range.length + 1); } } } return bytesInserted; } #pragma mark * Other request canonicalization /*! Canonicalise the request headers. * \param request The request to canonicalise. */ static void CanonicaliseHeaders(NSMutableURLRequest * request) { // If there's no content type and the request is a POST with a body, add a default // content type of "application/x-www-form-urlencoded". if ( ([request valueForHTTPHeaderField:@"Content-Type"] == nil) && ([[request HTTPMethod] caseInsensitiveCompare:@"POST"] == NSOrderedSame) && (([request HTTPBody] != nil) || ([request HTTPBodyStream] != nil)) ) { [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; } // If there's no "Accept" header, add a default. if ([request valueForHTTPHeaderField:@"Accept"] == nil) { [request setValue:@"*/*" forHTTPHeaderField:@"Accept"]; } // If there's not "Accept-Encoding" header, add a default. if ([request valueForHTTPHeaderField:@"Accept-Encoding"] == nil) { [request setValue:@"gzip, deflate" forHTTPHeaderField:@"Accept-Encoding"]; } // If there's not an "Accept-Language" headre, add a default. This is quite bogus; ideally we // should derive the correct "Accept-Language" value from the langauge that the app is running // in. However, that's quite difficult to get right, so rather than show some general purpose // code that might fail in some circumstances, I've decided to just hardwire US English. // If you use this code in your own app you can customise it as you see fit. One option might be // to base this value on -[NSBundle preferredLocalizations], so that the web page comes back in // the language that the app is running in. if ([request valueForHTTPHeaderField:@"Accept-Language"] == nil) { [request setValue:@"en-us" forHTTPHeaderField:@"Accept-Language"]; } } #pragma mark * API extern NSMutableURLRequest * CanonicalRequestForRequest(NSURLRequest *request) { NSMutableURLRequest * result; NSString * scheme; //assert(request != nil); // Make a mutable copy of the request. result = [request mutableCopy]; // First up check that we're dealing with HTTP or HTTPS. If not, do nothing (why were we // we even called?). scheme = [[[request URL] scheme] lowercaseString]; //assert(scheme != nil); if ( ! [scheme isEqual:@"http" ] && ! [scheme isEqual:@"https"]) { //assert(NO); } else { CFIndex bytesInserted; NSURL * requestURL; NSMutableData * urlData; static const CanonicalRequestStepFunction kStepFunctions[] = { FixPostSchemeSeparator, LowercaseScheme, LowercaseHost, FixEmptyHost, // DeleteDefaultPort, -- The built-in canonicalizer has stopped doing this, so we don't do it either. FixEmptyPath }; size_t stepIndex; size_t stepCount; // Canonicalise the URL by executing each of our step functions. bytesInserted = kCFNotFound; urlData = nil; requestURL = [request URL]; //assert(requestURL != nil); stepCount = sizeof(kStepFunctions) / sizeof(*kStepFunctions); for (stepIndex = 0; stepIndex < stepCount; stepIndex++) { // If we don't have valid URL data, create it from the URL. //assert(requestURL != nil); if (bytesInserted == kCFNotFound) { NSData * urlDataImmutable; urlDataImmutable = CFBridgingRelease( CFURLCreateData(NULL, (CFURLRef) requestURL, kCFStringEncodingUTF8, true) ); //assert(urlDataImmutable != nil); urlData = [urlDataImmutable mutableCopy]; //assert(urlData != nil); bytesInserted = 0; } //assert(urlData != nil); // Run the step. bytesInserted = kStepFunctions[stepIndex](requestURL, urlData, bytesInserted); // Note: The following logging is useful when debugging this code. Change the // if expression to YES to enable it. if (/* DISABLES CODE */ (NO)) { // fprintf(stderr, " [%zu] %.*s\n", stepIndex, (int) [urlData length], (const char *) [urlData bytes]); } // If the step invalidated our URL (or we're on the last step, whereupon we'll need // the URL outside of the loop), recreate the URL from the URL data. if ( (bytesInserted == kCFNotFound) || ((stepIndex + 1) == stepCount) ) { requestURL = CFBridgingRelease( CFURLCreateWithBytes(NULL, [urlData bytes], (CFIndex) [urlData length], kCFStringEncodingUTF8, NULL) ); //assert(requestURL != nil); urlData = nil; } } [result setURL:requestURL]; // Canonicalise the headers. CanonicaliseHeaders(result); } return result; }