how can I access iframe.contentDocument to get response after cross-origin request?

31,332

Solution 1

After digging around to try and figure this out, I finally came across a solution that seems to work for me. However, it isn't an exact answer to my question.

In summary, I'm working on supporting <form> based file uploads. For browsers that do not support file upload via XHR, we have to resort to the traditional <form> submission, using a hidden <iframe> to avoid page refresh. The form redirects the reload action into the hidden <iframe>, and then the HTTP response is written to the body of the <iframe> after the file has been transferred.

Due to the same-origin policy, and hence the reason I asked this question, I don't have access to the <iframe>'s content. The same-origin policy for <iframe>s restricts how a document or script loaded from one origin can interact with a resource from another origin. Since we can't get access to the <iframe>'s document, which is where the file upload HTTP response gets written to, the server will return a redirect response to which the server will append the upload response JSON. When the <iframe> loads, the JS will parse out the response JSON, and write it to the <iframe>'s body. Finally, since the redirect was to the same origin, we can access the <iframe>'s contents :)

A huge thanks to jQuery File Uploader; they did all the hard work ;)

https://github.com/blueimp/jQuery-File-Upload/wiki/Cross-domain-uploads

Setting Up...

JS

function setupForm() {

  // form is declared outside of this scope
  form = document.createElement('form');
  form.setAttribute('id', 'upload-form');
  form.setAttribute('target', 'target-iframe');
  form.setAttribute('enctype', 'multipart/form-data');
  form.setAttribute('method', 'POST');

  // set the 'action' attribute before submitting the form
  form.setAttribute('action', 'invalid');
};

function setupIframe() {

  // iframe is declared outside of this scope
  iframe = document.createElement('iframe');

  /*
   * iframe needs to have the 'name' attribute set so that some versions of
   * IE and Firefox 3.6 don't open a new window/tab 
   */
  iframe.id = 'target-iframe';
  iframe.name = 'target-iframe';

  /*
   * "javascript:false" as initial iframe src to prevent warning popups on
   * HTTPS in IE6
   */
  iframe.src = 'javascript:false;';
  iframe.style.display = 'none';

  $(iframe).bind('load', function() {
    $(iframe)
      .unbind('load')
      .bind('load', function() {
        try {

          /*
           * the HTTP response will have been written to the body of the iframe.
           * we're assuming the server appended the response JSON to the URL,
           * and did the redirect correctly
           */
          var content = $(iframe).contents().find("body").html();
          response = $.parseJSON(content);

          if (!response) {
            // handle error
            return;
          }

          uploadFile(...); // upload the next file
        }
        catch (e) {
          // handle error
        }
      });
  });

  /*
   * insert the iframe as a sibling to the form. I don't think it really
   * matters where the iframe is on the page
   */
  $(form).after(iframe);
};

HTML - redirect page

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script type="text/javascript">
      // grabs the JSON from the end of the URL
      document.body.innerHTML = decodeURIComponent(window.location.search.slice(1));
    </script>
  </body>
</html>

The only thing left to do is set the action attribute on the <form> to the cross-domain URL that we're sending the upload to:

form.setAttribute('action', 'http://sub.example.com:8080/upload?id=ab123');
form.submit();

Meanwhile, on the server...

// send redirect to the iframe redirect page, where the above HTML lives
// generates URL: http://example.com/hack?%7B%22error%22%3Afalse%2C%22status%22%3A%22success%22%7D
response.sendRedirect("http://example.com/hack?{\"error\":false,\"status\":\"success\"}");

I know this is a massive hack to get around the same-origin policy with <iframe>s, but it seems to work, and I think it's pretty good with cross-browser compatibility. I haven't tested it in all browsers, but I'll get around to doing that, and post an update.

Solution 2

Well, I came to solution also. It's a bit hard coded atm, but still looks neat.

First I made an html file on the server. (I'll modify it to ejs template later, that includes my data).

<!DOCTYPE html>
<html>
<title>Page Title</title>
<script>
    function myFunction() {
        parent.postMessage('Some message!!!', 'http://192.168.0.105:3001'); // hard coded, will change this later
    }

    window.onload=myFunction;
</script>
<body>
</body>
</html>

Important part here is the use of parent.

Than from my node server I'm uploading the file and sending back to the client the html file:

res.sendFile('file.html');

On the client I have the same html like you.

'<form id="{id}_form" action="http://192.168.0.105:3011/private/profile_picture/upload" enctype="multipart/form-data" method="post" target="{id}_uploadframe">',
'<span id="{id}_wrapper" class="file-wrapper">',
    '<input id="{id}_real" type="file" accept="image/*" name="photo" />',
    '<span class="button">{0}</span>',
'</span>',
'</form>',
'<iframe id="{id}_uploadframe" name="{id}_uploadframe" class="mc-hidden"></iframe>', 

This template I rendered on the page. I added the following event Handler also

window.addEventListener('message',function(event) {
    //if(event.origin !== cross_domain) return;
    console.log('message received:  ' + event.data,event);
},false);

As you know addEventListener is not working on all browsers. And this solution will not work on IE8 < 8, which does not support postMessage. Hope this is hapefull to you

Share:
31,332
Hristo
Author by

Hristo

LinkedIn JustBeamIt

Updated on February 26, 2020

Comments

  • Hristo
    Hristo about 4 years

    I'm successfully sending a file from localhost:8888 to localhost:8080 (different domain in production), but I can't read the HTTP response after the transfer finishes.

    Uncaught SecurityError: Failed to read the 'contentDocument' property from 'HTMLIFrameElement': Blocked a frame with origin "http://localhost:8888" from accessing a frame with origin "http://localhost:8080". The frame requesting access set "document.domain" to "localhost", but the frame being accessed did not. Both must set "document.domain" to the same value to allow access.

    To send the file, for compatibility support, I'm trying to get this to work for <form> based file uploads; not XHR based. This is the basic HTML structure:

    <form target="file-iframe" enctype="multipart/form-data" method="POST" action="invalid">
      <input type="file" id="file-input" class="file-input" title="select files">
    </form>
    <iframe src="javascript:false;" id="file-iframe" name="file-iframe"></iframe>
    

    To insert the <iframe> element into the DOM, I do the following:

    document.domain = document.domain;
    var domainHack = 'javascript:document.write("<script type=text/javascript>document.domain=document.domain;</script>")';
    
    var html = '<iframe id="file-iframe" name="file-iframe"></iframe>';
    var parent = document.getElementById('wrapper');
    var iframe = UTILS.createDomElement(html, parent);
    iframe.src = domainHack;
    
    UTILS.attachEvent(iframe, 'load', function(e) {
    
      // this throws the above SecurityError
      var doc = iframe.contentDocument || iframe.contentWindow.document;
    
      // do other stuff...
    });
    

    Before I submit the form, I set the action attribute on the <form> to be the target cross-domain URL:

    action="http://localhost:8080/"
    

    After submitting the <form>, the <iframe>'s load event is fired, and I try to access the <iframe>'s content to read the HTTP response. However, doing so throws the above error since this is is a cross-origin request, and I don't have access to the <iframe>'s content.

    I thought the document.domain hack would work, but the error message is telling me that the iframe did not set the domain to localhost, even though I set the iframe's src attribute to the domainHack variable, which seems to execute.

    Any ideas as to what I might be doing wrong? How can I set document.domain to be localhost for both the <iframe> and its parent (which is the current page).


    I've read through several StackOverflow questions, some MDN articles, and other random results on Google, but I haven't been able to get this to work. Some stuff I've looked through already: