electron / electron

:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS
https://electronjs.org
MIT License
113.02k stars 15.17k forks source link

[Feature Request]: protocol.intercept{Any}Protocol handler ability to call original handler #15434

Open dsanders11 opened 5 years ago

dsanders11 commented 5 years ago

Is your feature request related to a problem? Please describe. It's possible to intercept schemes (including built-ins like http:, https:, and file:), but doing so requires you to handle all requests for that scheme, there's no way to fall-through to the original handler as far as I can tell. Neglecting to call callback(...) simply makes the request hang.

Using protocol.interceptHttpProtocol('http', ...) will lead to an infinite loop if you use callback(request). This prevents using the intercepts to do handy things like set a header on all HTTP requests to a certain domain, or blackholing another (useful for preventing tracking, ads, etc):

const url = require('url')

protocol.interceptHttpProtocol('https', (request, callback) => {
  const { host, pathname } = url.parse(request.url)

  if (host === 'www.github.com') {
    request.headers['X-Foo'] = 'bar'
  } else if (host === 'www.bitbucket.org') {
    callback({ statusCode: 403 })
    return
  }

  callback(request)  // This will cause an infinite loop
})

Being able to selectively handle requests for an intercepted protocol would be very useful. It would provide the ability to use protocol.interceptFileProtocol('http', ...) and only intercept certain requests, such as those with the origin http://localhost/. This would allow you to simulate local files as being served over HTTP, getting around several of the limitations of the file:// protocol. Currently there's no easy way to do this as you'll intercept all HTTP requests and lose the ability to talk to the network (at least over HTTP). Even if your handler could retrieve the content, it would have to give it a file path so that the callback can return the path to the file, or you'd need to use the string/buffer/stream variants and open any files yourself and provide the content that way. Neither is very clean. You can also bundle a local web server, but this is overkill most of the time.

I've found this particularly painful for trying to get WebAuthn working without a server in the loop, as Chromium only allows the API for https domains, or http://localhost, not file:// (or any other protocol). An intercept seems like a perfect fit for this, except you can't selectively handle that small case.

Selective handling also opens the ability to make protocol handlers in the future act more like middleware which is composable and reusable. If interceptors could be applied like a stack and pass to the next interceptor, you could have libraries which handle things like setting certain headers.

Describe the solution you'd like I think this can be solved with a somewhat minor change to the API. There may be dragons in the implementation, I've glanced at the code which implements the protocol module, but I'm not too familiar with it.

My suggested solution would be to add a third argument to the handler function for the protocol.intercept{Any}Protocol functions called next, and rename the callback argument to done. done would have the same behavior as the current callback but the name change makes it more explicit that it will end processing of the request. The new argument, next, would fall through to the original handler, the one which would be restored by protocol.uninterceptProtocol. next would optionally take a request object, allowing you to modify it before passing it along, such as setting a header, or changing the URL allowing redirecting between files.

So, the intercept earlier which currently causes an infinite loop could be written as:

const url = require('url')

protocol.interceptHttpProtocol('https', (request, done, next) => {
  const { host, pathname } = url.parse(request.url)

  if (host === 'www.github.com') {
    request.headers['X-Foo'] = 'bar'
  } else if (host === 'www.bitbucket.org') {
    callback({ statusCode: 403 })
    return
  }

  next(request)
})

You could also write a handler which pretended to be http://localhost by reading files from disk:

const url = require('url')

protocol.interceptFileProtocol('http', (request, done, next) => {
  const { host, pathname } = url.parse(request.url)

  if (host === 'localhost') {
    done({ path: path.normalize(`${__dirname}/${pathname}`) })
  } else {
    next()  // Invokes the original 'http:' handler for 'request'
  }
})

Or make file:// check cloud storage if the file isn't found locally:

const fs = require('fs')
const url = require('url')

protocol.interceptStreamProtocol('file', (request, done, next) => {
  let { host, pathname } = url.parse(request.url)
  pathname = pathname.slice(1)

  if (host === '') {
    // Absolute path, check if pathname exists, if not
    // check cloud storage (Dropbox, OneDrive, etc)
    if (fs.existsSync(pathname)) {
      next()  // Invoke the original 'file:' handler for 'request'
    } else {
      checkCloud(contents => {
        if (contents) {
          done(/* stream to contents */)
        } else {
          done({ statusCode: 404, data: 'Not Found' })
        }
      })
    }
  } else {
    // Don't allow file:// with a host
    done({ statusCode: 400, data: 'Host Not Allowed' })
  }
})

Combined with intercepting a custom protocol:

const url = require('url')

protocol.registerFileProtocol('atom', (request, callback) => {
  const { host, pathname } = url.parse(request.url)

  callback({ path: path.normalize(`${__dirname}/${host + pathname}`) })
})

protocol.interceptStreamProtocol('atom', (request, done, next) => {
  const { host, pathname } = url.parse(request.url)

  if (pathname.endsWith('.mp4') || pathname === '/dev/video') {
    done(/* Open a stream to video */)
  } else {
    next()
  }
})
burtonator commented 5 years ago

YES!! Would LOVE to have this functionality!

njbmartin commented 5 years ago

Just to add to this, I've been using Puppeteer for my functional tests and it has the following:

await page.setRequestInterception(true);
  page.on('request', interceptedRequest => {
    if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg'))
      interceptedRequest.abort();
    else
      interceptedRequest.continue();
  });

So theoretically it should be possible to lift and shift some of the necessary code to achieve this behaviour.

deepakanand commented 5 years ago

Fwiw, this has precedent and the CEFSharp API provides an API to intercept specific URLs and not the scheme wholesale: https://github.com/cefsharp/CefSharp/wiki/General-Usage#loading-htmlcssjavascriptetc-from-diskdatabaseembedded-resourcestream

ahm-3-d commented 4 years ago

Any updates on this being implemented?

zbot473 commented 4 years ago

Is there any alternative for modifying requests?

johannesjo commented 4 years ago

Not sure if I am reading this correctly, but to me it seems like that this is a chrome functionality they are just wrapping on electrons side of things. If this is true, this might not change it if chrome doesn't change it.

I would love to be wrong about this, though. Having an alternative to either run a local web server or to serve from the file protocol would be great, as both approaches causing all sorts of issues (service workers, third party authentication flows, not being able to occupy the same local port,etc).

mahnunchik commented 4 years ago

Any news?

TheSHEEEP commented 3 years ago

This would really be extremely helpful. The current webRequest.onBeforeXXX doesn't really allow you to handle all the cases and from the documentation it isn't clear at all how you could intercept only SOME requests, while letting all others handle normally.

walkthunder commented 3 years ago

Almost two years, No reponse

tarruda commented 3 years ago

This is not the same of what the OP requested, but an alternative is passing an array of URL patterns as second argument to intercept{Any}Protocol which limits the callback to intercept only URLs that match the patterns. Here's a patch to anyone interested: https://gist.github.com/tarruda/554962f987f7f3a1a94d2c1c76c880a8#file-allow-url-patterns-intercept-protocol-diff-L186

neuralisator commented 3 years ago

Sorry for confusing anyone with the previously posted code excerpt (deleted) for a single page / single file app. There was more going on the code so it just appeared as if it worked, but manual interception of the first request was actually done, and a page reload had to be done twice for the app to actually load.

I'm now using the code below, which intercepts the following (first) request and delivers the app code, whenever the page is loaded (or reloaded).

let internal_url = 'https://my-internal-domain/webcontent.html';

win.webContents.on('did-start-loading', function(e) {
  protocol.interceptBufferProtocol('https', function(request, respond) {
    protocol.uninterceptProtocol('https');
    if (request.url !== internal_url) {
      console.warn('something went wrong');
    } else {
      let content = fs.readFileSync(app_folder + '/webcontent.html');
      respond(content);
    }
  });
});

win.loadURL(internal_url);
famingyuan commented 3 years ago

Sorry for confusing anyone with the previously posted code excerpt (deleted) for a single page / single file app. There was more going on the code so it just appeared as if it worked, but manual interception of the first request was actually done, and a page reload had to be done twice for the app to actually load.

I'm now using the code below, which intercepts the following (first) request and delivers the app code, whenever the page is loaded (or reloaded).

let internal_url = 'https://my-internal-domain/webcontent.html';

win.webContents.on('did-start-loading', function(e) {
  protocol.interceptBufferProtocol('https', function(request, respond) {
    protocol.uninterceptProtocol('https');
    if (request.url !== internal_url) {
      console.warn('something went wrong');
    } else {
      let content = fs.readFileSync(app_folder + '/webcontent.html');
      respond(content);
    }
  });
});

win.loadURL(internal_url);

thanks,@neuralisator . I also add a flag , orelse it will go to the 'something went wrong' when I send other XHR request after the index page loaded.

let isEverReg = false
    win.webContents.on('did-start-loading', function () {
      if (isEverReg === true) {
        return
      }
      isEverReg = true
      protocol.interceptBufferProtocol('http', function (request, respond) {
        protocol.uninterceptProtocol('http')
        if (request.url !== URL) {
          mainLogger.info('something went wrong:' + request.url)
        } else {
          const filePath = `${join(__dirname, '../../dist/renderer/index.html')}`
          const fileContent = readFileSync(filePath)
          mainLogger.info('Index page read successfully.')
          respond({
            statusCode: 200,
            data: fileContent
          })
        }
      })
    })
hxkuc commented 3 years ago

any news

e9x commented 3 years ago

We are helpless

jianzhou520 commented 2 years ago

hello?

eyun-tv commented 2 years ago

any update ?

MrMebelMan commented 2 years ago

Updaaaaaaaaate pls :pleading_face:

carrytao commented 2 years ago

any updates?

oohusl commented 2 years ago

any updates or plan?

richardo2016 commented 1 year ago

@zcbenz Hi! I have searched for a while to resolve my problem: I need to intercept http protocol ONLY for specific urls. I think suggestion posted in this issue is one acceptable resolution.

Maybe we should try patch in this comment? I fully understand that for the electron team, this feature may not be the highest priority, but it may be important for the developers in this issue. If possible, I would like to receive an official response: will the patch in this thread be considered? If so, I can help push for this change.

I have not compiled chromium or electron myself before, only v8. I know that compiling chromium-based applications is a long process. But to solve this problem, I will pull the source code soon and set up the necessary tools such as gclient/gn/ninja, and then try some modifications. If the solution is feasible, I hope to receive official feedback and assistance at that time.

FatShen3 commented 1 year ago

any news ? In new electron version, cookie samesite=none must be used with https while our server is still http. Our project use local html files and request the http server, so the set-cookie header is useless.

oohusl commented 1 year ago

It seems v25 to fix the issue.

https://github.com/electron/electron/blob/main/docs/breaking-changes.md

Deprecated: protocol.{register,intercept}{Buffer,String,Stream,File,Http}Protocol The protocol.registerProtocol and protocol.interceptProtocol methods have been replaced with protocol.handle.

The new method can either register a new protocol or intercept an existing protocol, and responses can be of any type.

SagaciousLittle commented 1 year ago

request headers are also important, and existing apis don't get them right

KaKi87 commented 2 months ago

Hello, Any news on this ? Thanks