apache / cordova-ios

Apache Cordova iOS
https://cordova.apache.org/
Apache License 2.0
2.15k stars 987 forks source link

XMLHttpRequest CORS issue on read remote file (image) #1299

Closed FranGhe closed 1 year ago

FranGhe commented 1 year ago

When building app everything is ok... I have many ajax call to remote file (php) and everything it's ok because i can manage header("Access-Control-Allow-Origin: ...."); But the issue arises when I have to read/download images files.

On config.xml I set

<preference name="hostname" value="localhost" />
<preference name="scheme" value="https" /> 

And reading some post I also installed @ahovakimyan/cordova-plugin-wkwebviewxhrfix

This is my code:

  var req = new XMLHttpRequest();
  req.open("GET", "https://www.mySite.xx/img/logo.jpg", true);
  req.responseType = "blob";
  req.onload = function (event) {
      var blob = req.response;
      var storageLocation = cordova.file.dataDirectory;

      if(storageLocation){
          window.resolveLocalFileSystemURL(
              storageLocation, 
              function (directoryEntry) {
                  directoryEntry.getFile(
                      pathDest,
                      { create: true }, 
                      function (fileEntry) {
                          fileEntry.createWriter(
                              function (fileWriter) {
                                  fileWriter.write(blob);
                                  fileWriter.onwriteend = function (e) { 
                                      //console.log('Write of file completed.'); 
                                      var split=pathDest.split("-");
                                      var cont=split[0];
                                      var thisContent=allAppData[cont];
                                      if(thisContent["temporaryImages"]){
                                          thisContent.temporaryImages[key]=storageLocation+pathDest;
                                      }
                                      else{
                                          thisContent.temporaryImages={};
                                          thisContent.temporaryImages[key]=storageLocation+pathDest;
                                      }
                                  };
                                  fileWriter.onerror = function (e) { console.log("downloadTemporaryFile -> Err01"); };
                              },
                              function (err) {
                                  console.log("downloadTemporaryFile -> Err02");
                                  console.error(err);
                              }
                          );
                      },
                      function (err) {
                          console.log("downloadTemporaryFile -> Err03");
                          console.error(err);
                      }
                  );
              },
              function (err) {
                  console.log("downloadTemporaryFile -> Err04");
                  console.error(err);
              }
          );
      }
  };
req.send();

On my console I have these errors: [Error] Origin app://localhost is not allowed by Access-Control-Allow-Origin. Status code: 200 [Error] XMLHttpRequest cannot load https://www.mySite.xx/img/logo.jpg due to access control checks. [Error] Failed to load resource: Origin app://localhost is not allowed by Access-Control-Allow-Origin. Status code: 200

I use the same code for Android an there aren't issue... How can I solve it?

Thanks in advance

breautek commented 1 year ago

[Error] Origin app://localhost is not allowed by Access-Control-Allow-Origin. Status code: 200

This indicates that your server is not returning app://localhost in it's Access-Control-Allow-Origin response.

<preference name="scheme" value="https" />

This is an invalid configuration for iOS, although it's valid for Android. Android scheme must be either http or https, whereas iOS can be anything that isn't a "well-known scheme". Apple doesn't explicitly declare what that means, but virtually any standardized protocol is an illegal scheme. This means that you can't have a consistent origin cross-platform, since the two platforms have conflicting requirements. When iOS encounters an illegal scheme, it fallsback to app, which is why you're origin becomes app://localhost despite the scheme being set to https. Leaving it as is ok, but just explaining why your origin is app://localhost instead of https://localhost.

You haven't told us what you have your Access-Control-Allow-Origin to, so for the moment I'm going to assume it's not app://localhost... more on this later...

On the server side, because you support both Android and iOS also means you need to be flexible and return a different value depending on the Origin request header. Any CORS request will have a Origin header on the request, which you can pass back as the Access-Control-Allow-Origin to make it act as a wildcard. Additionally you could check to see if Origin is what you expect, if you like. The Access-Control-Allow-Origin only accepts 1 value, but that value can be a * for a wildcard. I've had issues with this method on older iOS devices however (pre iOS 10). I haven't actually tested if it works properly on iOS 11+.

You haven't told us what you have your Access-Control-Allow-Origin to, so for the moment I'm going to assume it's not app://localhost... more on this later...

If your GET request is returning Access-Control-Allow-Origin response header with the value of app://localhost, then you might be triggering a preflight CORS request, which may happen if certain conditions are met. MDN goes into more detail on these conditions.

If your request requires a preflight request, then your server must respond to the OPTIONS request in addition to the GET (or whatever HTTP Method your main API endpoint is)

The OPTIONS request shall respond with:

You can test your endpoints to confirm if the server is responding properly with the proper response headers using a non-browser based HTTP client. Personally I use Postman for this.

I have a blog post that goes into much more detail on how to handle CORs, with NGINX config examples, but the general concept should be able to be applied to any webserver technology.

FranGhe commented 1 year ago

Thanks for your support... talking about the request from my app to my server to a file php there isn't problem because this is my php code:

<?php
header('Access-Control-Allow-Origin: *');
...

my problem is only when I want read an image file to download... as example: https://www.mySite.xx/img/logo.jpg https://www.mySite.xx/img/img1.jpg https://www.mySite.xx/img/img2.png ... ecc

in this case how I can do to not have CORS problem?... I can't write in a binary file...

FranGhe commented 1 year ago

This is the solution... no one setting server side...

Install cordova-plugin-wkwebview-file-xhr and cordova-plugin-wkwebviewxhrfix plugins

On config.xml set:

<platform name="ios">    
  <preference name="scheme" value="app" />
  <preference name="hostname" value="localhost" />
  ...
</platform>

Than, to download the files:

function downloadTemporaryFile(pathOrig, key, pathDest) {
  var req = new XMLHttpRequest();
  req.open("GET", pathOrig, true);
  req.responseType = "blob";
  req.onload = function (event) {
      var blob = req.response;
      var storageLocation = cordova.file.dataDirectory;

      if(storageLocation){
          window.resolveLocalFileSystemURL(
              storageLocation, 
              function (directoryEntry) {
                  directoryEntry.getFile(
                      pathDest,
                      { create: true }, 
                      function (fileEntry) {
                          fileEntry.createWriter(
                              function (fileWriter) {
                                  fileWriter.write(blob);
                                  fileWriter.onwriteend = function (e) { 
                                      //console.log('Write of file completed.'); 
                                      var split=pathDest.split("-");
                                      var cont=split[0];
                                      var thisContent=allAppData[cont];
                                      var localFile = storageLocation+pathDest;
                                      if(!thisContent["temporaryImages"]){
                                          thisContent.temporaryImages={};
                                      }
                                      if(device.platform === "Android"){
                                          thisContent.temporaryImages[key]=localFile;
                                      }
                                      else if(device.platform === "iOS"){
                                          thisContent.temporaryImages[key]=window.WkWebView.convertFilePath(localFile);
                                      }
                                  };
                                  fileWriter.onerror = function (e) { console.log("downloadTemporaryFile -> Err01"); };
                              },
                              function (err) {
                                  console.log("downloadTemporaryFile -> Err02");
                                  console.error(err);
                              }
                          );
                      },
                      function (err) {
                          console.log("downloadTemporaryFile -> Err03");
                          console.error(err);
                      }
                  );
              },
              function (err) {
                  console.log("downloadTemporaryFile -> Err04");
                  console.error(err);
              }
          );
      }
  };
req.send();
}
breautek commented 1 year ago

Thanks for your support... talking about the request from my app to my server to a file php there isn't problem because this is my php code:

<?php
header('Access-Control-Allow-Origin: *');
...

my problem is only when I want read an image file to download... as example: https://www.mySite.xx/img/logo.jpg https://www.mySite.xx/img/img1.jpg https://www.mySite.xx/img/img2.png ... ecc

in this case how I can do to not have CORS problem?... I can't write in a binary file...

If you're responding with the header Access-Control-Allow-Origin: * on both the GET and the OPTIONS requests and it still isn't working with the native XMLHttpRequest/fetch APIs, then you might be hitting an issue I mentioned:

I've had issues with this method on older iOS devices however (pre iOS 10). I haven't actually tested if it works properly on iOS 11+.

I know for certain that iOS 10 and earlier didn't properly support wildcards, and based on what you're telling me that might still be the case with current iOS versions. If that's the situation then the proper solution would be to return the domain as read from the Origin request header, which would simulate a wildcard behaviour.

Install cordova-plugin-wkwebview-file-xhr and cordova-plugin-wkwebviewxhrfix plugins

Using these plugins can work as well. They generally work by overwriting the browser's HTTP apis so that requests are made using the native HTTP layer rather than the browser's HTTP layer. This works because the native device HTTP APIs are not bounded by CORs since CORs is a browser concept. However, this is working around CORs instead of being CORS compliant.

If this is the solution you have decided to go with however, I'll close this issue.

FranGhe commented 1 year ago

I know for certain that iOS 10 and earlier didn't properly support wildcards, and based on what you're telling me that might still be the case with current iOS versions. If that's the situation then the proper solution would be to return the domain as read from the Origin request header, which would simulate a wildcard behaviour.

Like this?:

if (isset($_SERVER['HTTP_ORIGIN'])) {
    header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Max-Age: 86400');    // cache for 1 day
}

if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
    if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])){
        header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
    }         
    if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])){
        header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
    }
}
breautek commented 1 year ago

I'm not too familiar with PHP but that looks mostly right, however on the OPTIONS you shouldn't be responding any content body back, so if this php script is handling both the GET and OPTIONS methods, then you'll need to return early without sending back the image. On the OPTIONS method, the Content-Length should be 0 and there should be no payload, Just the response headers.

In othewords, you should only return the image content if the HTTP method is the GET request. I'm not sure how to do that in PHP myself, so I can't provide an example.