Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cross-origin URL redirection workaround #909

Merged
merged 1 commit into from Oct 13, 2016
Merged

Conversation

@jablko
Copy link
Contributor

jablko commented Sep 13, 2016

Browsers allow HTTP redirects for simple cross-origin requests but not for requests that require preflight.
Because they set the Range header, this includes all web seed requests (if they're cross-origin).
So currently, web seeds can be cross-origin or they can make redirects, but not both at the same time.
What do you think of the following workaround for this?

If a request fails, maybe it's because the web seed sent a redirect and the browser objected, so try a simple request.
Make a simple request to unwind any redirects and get the final URL. (I used a HEAD request for this.)
If the final URL doesn't match the original URL, retry our original request with the new URL.
I make a feeble attempt to check that this is a cross-origin request.
It catches all cross-origin cases, but matches a few same-origin cases too. I figure that's okay in the interest of simplicity, since the check is merely an optimization anyway.

Copy link
Member

josephfrazier left a comment

I spent little while learning about the browser issue here (it boils down to Range not being a "simple header"), and it sounds like you're pretty close to fixing it!

Other than the inline comments I made, another suggestion I have is that the window.fetch logic (both detection and usage) could be broken out into its own function, so there's less code nesting.

Note to self: you can reproduce the browser issue by running this in the console at https://example.com:

fetch('https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2F', {
  headers: {
    Range: 'bytes=0-'
  }
})
// expose the final URL.
// fetch() or XMLHttpRequest.responseUrl are both candidates.
if (typeof window !== 'undefined' && typeof window.fetch === 'function' && !url.startsWith(window.location.origin + '/')) {
window.fetch(url, { method: 'HEAD' }).then(function (res) {

This comment has been minimized.

Copy link
@josephfrazier

josephfrazier Oct 7, 2016

Member

Shouldn't there be a .catch() also?

This comment has been minimized.

Copy link
@jablko

jablko Oct 11, 2016

Author Contributor

Oops. Yes. Fixed. Thanks!

if (typeof window !== 'undefined' && typeof window.fetch === 'function' && !url.startsWith(window.location.origin + '/')) {
window.fetch(url, { method: 'HEAD' }).then(function (res) {
if (hasError) return
if (res.url !== url) {

This comment has been minimized.

Copy link
@josephfrazier

josephfrazier Oct 7, 2016

Member

I think res.ok should be checked as well:

            if (res.ok && res.url !== url) {

This comment has been minimized.

Copy link
@jablko

jablko Oct 11, 2016

Author Contributor

Good point. Fixed.

}

hasError = true
return cb(err)

This comment has been minimized.

Copy link
@josephfrazier

josephfrazier Oct 7, 2016

Member

Actually, it might be better to handle the error case with an early exit. So, lines 165-180 could be replaced by:

            if (!res.ok || res.url === url) {
              hasError = true
              return cb(err)
            }

            opts.url = res.url
            get.concat(opts, function (err, res, data) {
              if (hasError) return
              if (err) {
                hasError = true
                return cb(err)
              }
              onResponse(res, data)
            })

This comment has been minimized.

Copy link
@jablko

jablko Oct 11, 2016

Author Contributor

Done.

}

hasError = true
return cb(err)

This comment has been minimized.

Copy link
@josephfrazier

josephfrazier Oct 7, 2016

Member

I'm tempted to say this should be an early-exit as well (at line 162):

        if (typeof window === 'undefined' || typeof window.fetch !== 'function' || url.startsWith(window.location.origin + '/')) {
          hasError = true
          return cb(err)
        }

        // (the code at line 163)

This comment has been minimized.

Copy link
@jablko

jablko Oct 11, 2016

Author Contributor

Done.

// never falsely positive.
//
// We can't use simple-get here because http-browserify doesn't
// expose the final URL.

This comment has been minimized.

Copy link
@josephfrazier

josephfrazier Oct 8, 2016

Member

I came back to this, and I now think we actually can use simple-get here. browserify replaced http-browserify with stream-http (see the list of shims here), which does expose res.url (see jhiesey/stream-http#40).

Here's a proof of concept that you can browserify and run in the console at https://example.com:

var get = require('simple-get')

function getPreflightRedirectConcat(opts, cb) {
  return get.concat(opts, function (err, res, data) {
    if (!err || opts.url.startsWith(window.location.origin + '/')) {
      return cb(err, res, data)
    }

    return get.head(opts.url, function (errHead, resHead) {
      // If the HEAD didn't work or didn't redirect, return the original error
      if (errHead || resHead.url == opts.url) {
        return cb(err, res, data)
      }

      var opts2 = Object.assign({}, opts)
      opts2.url = resHead.url
      return get.concat(opts2, cb)
    })
  })
}

var opts = {
  url: 'https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2F',
  headers: {
    Range: 'bytes=0-'
  }
}
getPreflightRedirectConcat(opts, function (err, res, data) {
  if (err) {
    console.error(err)
  }
  else {
    console.log("final url: " + res.url)
    console.log(data.toString())
  }
  window.close()
})

This comment has been minimized.

Copy link
@jablko

jablko Oct 11, 2016

Author Contributor

Oh, excellent! I switched to using simple-get.

@jablko jablko force-pushed the jablko:master branch from 1371277 to 2c19331 Oct 11, 2016
@jablko

This comment has been minimized.

Copy link
Contributor Author

jablko commented Oct 11, 2016

Thanks a lot for reviewing this!
I'm definitely not opposed to breaking this out into its own function, I just don't yet see how to do it in a way that improves clarity or simplicity.

@josephfrazier

This comment has been minimized.

Copy link
Member

josephfrazier commented Oct 13, 2016

You're welcome! I think it looks pretty good now. It could still be broken into its own function (like the one I posted above), but that can happen later if needed.

We don't exactly have any tests for this part of the code, so can you confirm that it still fixes your use case?

@jablko

This comment has been minimized.

Copy link
Contributor Author

jablko commented Oct 13, 2016

Excellent! I checked manually and confirm it still resolves the issue.

@josephfrazier josephfrazier merged commit 1072de2 into webtorrent:master Oct 13, 2016
3 checks passed
3 checks passed
Node Security No known vulnerabilities found
Details
continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
@josephfrazier

This comment has been minimized.

Copy link
Member

josephfrazier commented Oct 13, 2016

Merged, thanks!

@jablko

This comment has been minimized.

Copy link
Contributor Author

jablko commented Oct 13, 2016

Thank you!

@feross

This comment has been minimized.

Copy link
Member

feross commented Oct 31, 2016

LGTM!

@lock

This comment has been minimized.

Copy link

lock bot commented May 4, 2018

This thread has been automatically locked because it has not had recent activity. To discuss futher, please open a new issue.

@lock lock bot locked as resolved and limited conversation to collaborators May 4, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Linked issues

Successfully merging this pull request may close these issues.

None yet

3 participants
You can’t perform that action at this time.