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  }