Simplest unpacked extension to fetch() localhost on any URL from console

144 views
Skip to first unread message

guest271314

unread,
May 6, 2021, 10:00:44 AM5/6/21
to Chromium Extensions
What is the simplest unpacked extension (manifest; background) for the ability to fetch() localhost (developmental server) from console at any URL (origin)?

I am currently dynamically setting externally_connectable by updating manifest.json then reloading the extension. 


ilyaigpetrov

unread,
May 8, 2021, 10:11:30 AM5/8/21
to Chromium Extensions, guest...@gmail.com
Hi.
It's not clear what you are trying to build.
1) You have an api server running on localhost:somePort?
2) You want to access this server from any page's console via `fetch`?
3) How do your fetches look like? Is it like: `fetch('http://localhost:somePort/method?query')` or `fetch('chromium-extension://someFileFromExtension')`?

guest271314

unread,
May 8, 2021, 11:22:29 AM5/8/21
to Chromium Extensions, ilyaigpetrov, guest271314
> It's not clear what you are trying to build.

A. Workaround for Web Speech API a) not supporting capture of audio output; b) not supporting SSML parsing;

B. Pass code to a shell, execute arbitrary local applications and shell scripts and pipe STDOUT to browser in "real-time".

> 1) You have an api server running on localhost:somePort?

Yes.

<?php 
if (isset($_POST["tts"])) {
    print($_GET["tts"]);
    header('Vary: Origin');
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: GET, POST, OPTIONS, HEAD");
    header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers");    
    header("Content-Type: text/plain");
    header("X-Powered-By:");
    echo passthru($_POST["tts"]);
    exit();
  }

> 2) You want to access this server from any page's console via `fetch`?

Yes.

If this is not possible due to CSP, etc. then this question/issue should be construed as a feature request, something like the ability to set a URL such as 'chrome://dev' to 'allowlist' for any URL that is also outside the restrictions of sites' CSP, where 'chrome://dev' is mapped to a local directory.

> 3) How do your fetches look like? Is it like: `fetch('http://localhost:somePort/method?query')` or `fetch('chromium-extension://someFileFromExtension')`?

Current manifest.json is as simple as possible

{
  "name": "Hello, World!",
  "version": "1.0",
  "manifest_version": 3,
  "externally_connectable": {
    "ids": [
      "*"
    ],
    "matches": [
    ]
  },
  "background": {
    "service_worker": "background.js"
  }
}

I navigate to a page then dynamically update 'manifest.json' using File System Access API

(async(set_externally_connectable = ["https://example.com/*"], unset_externally_connectable = true) => {
  const dir = await self.showDirectoryPicker();
  const status = await dir.requestPermission({mode: 'readwrite'});
  const fileHandle = await dir.getFileHandle("manifest.json", {create: false});
  const file = await fileHandle.getFile();
  const manifest_text = await file.text();
  let text = manifest_text;
  let extension_id;
  try {
    const match_extension_id = /\/\/ Extension ID: \w{32}/;
    ([extension_id] = manifest_text.match(match_extension_id));
    text = manifest_text.replace(match_extension_id, `"_": 0,`);
  } catch (err) {
    console.error(err);
  }
  const manifest_json = JSON.parse(text);
  manifest_json.externally_connectable.matches = unset_externally_connectable ? set_externally_connectable :
    [...manifest_json.externally_connectable.matches, ...set_externally_connectable];
  const writer = await fileHandle.createWritable({keepExistingData:false});
  let updated_manifest = JSON.stringify(manifest_json, null, 2);
  if (extension_id) {
    updated_manifest = updated_manifest.replace(/"_": 0,/, extension_id);
  }
  await writer.write(updated_manifest); 
  return await writer.close();
})([`${location.origin}/*`]);

One way to do this at background.js the fetch() call would look something like

chrome.runtime.onConnectExternal.addListener(externalPort => {
    externalPort.onMessage.addListener(async (message) => { 
       var fd = new FormData();      
       fd.append('tts', message);
       const res = await fetch('http://localhost:8000', {method:'post', body:fd});
       const value = new Uint8Array(await res.arrayBuffer());     
            externalPort.postMessage(
              value
            );  
    });
  });

where /* do stuff */ is, in this case, essentially this code https://github.com/guest271314/webtransport/blob/main/webTransportBreakoutBox.js, which is working code. The issue is QuicTransport and quic-transport URL scheme are deprecated, so this code https://github.com/guest271314/webtransport/blob/main/quic_transport_server_tts.py no longer works at tip-of-tree Chromium.

What I am trying to avoid here is navigating to 'chrome://extensions' manually reloading the extension so that I can next run

var id = 'moanlldbieaaopggnjbojimgkkikbmen'
var port = chrome.runtime.connect(id);
port.onMessage.addListener((e) => {  /* do stuff */ });
port.postMessage(`espeak-ng -m --stdout -v 'Storm' "test"`);

at console.

guest271314

unread,
May 8, 2021, 11:29:34 AM5/8/21
to Chromium Extensions, guest271314, ilyaigpetrov
At a different extension test I am able to fetch() directly, without using externally_connectable, which is what I am trying to do. 


{
  "name": "No tiles",
  "version": "1.0",
  "manifest_version": 3,
  "chrome_url_overrides": {
    "newtab": "./hello.html"
  },
  "background": {
    "service_worker": "background.js"
  }
}

On the chrome://extension URL I am able to fetch() localhost:port directly at console, avoiding messaging (text, not stream) altogether

var fd = new FormData();
fd.append('tts', `espeak-ng -m --stdout -v 'Storm' "test"`)
fetch('http://localhost:8000', {method:'post', body:fd})
.then(r => r.body).then(async (readable) => {
    const initial = 1; // 6.4KiB (65536)
    const maximum = 500; // 32 KiB
    let readOffset = 0;
    let writeOffset = 0;
    let duration = 0;
    let init = false;
    // TODO process odd length Uint8Array without writing to Memory
    const memory = new WebAssembly.Memory({
      initial,
      maximum,
      shared: true,
    });

    const ac = new AudioContext({
      sampleRate: 22050,
      latencyHint: 0,
    });
    await ac.suspend();
    const msd = new MediaStreamAudioDestinationNode(ac, {
      channelCount: 1,
    });

    const { stream } = msd;
    const [track] = stream.getAudioTracks();
    const osc = new OscillatorNode(ac, { frequency: 0 });
    const processor = new MediaStreamTrackProcessor(track);
    const generator = new MediaStreamTrackGenerator({ kind: 'audio' });
    const { writable } = generator;
    const { readable: audioReadable } = processor;
    const audioWriter = writable.getWriter();
    const mediaStream = new MediaStream([generator]);
    const audioReader = audioReadable.getReader();
    const source = new MediaStreamAudioSourceNode(ac, {mediaStream});
    source.connect(ac.destination);
    osc.connect(msd);
    osc.start();
    track.onmute = track.onunmute = track.onended = (e) => console.log(e);
    // const recorder = new MediaRecorder(mediaStream);
    // recorder.ondataavailable = ({ data }) => console.log(URL.createObjectURL(data));
    // recorder.start();
    return await Promise.all([
      readable.pipeTo(
        new WritableStream({
          async write(value, c) {
            console.log(
              `Uint8Array.buffer.byteLength: ${value.buffer.byteLength}`
            );
            if (readOffset + value.byteLength > memory.buffer.byteLength) {
              console.log(
                `memory.buffer.byteLength before grow(): ${memory.buffer.byteLength}.`
              );
              memory.grow(3);
              console.log(
                `memory.buffer.byteLength after grow(): ${memory.buffer.byteLength}`
              );
            }
            let sab = new Int8Array(memory.buffer);
            let i = 0;
            if (!init) {
              init = true;
              i = 44;
            }
            for (; i < value.buffer.byteLength; i++, readOffset++) {
              if (readOffset + 1 >= memory.buffer.byteLength) {
                console.log(
                  `memory.buffer.byteLength before grow() for loop: ${memory.buffer.byteLength}.`
                );
                memory.grow(3);
                console.log(
                  `memory.buffer.byteLength after grow() for loop: ${memory.buffer.byteLength}`
                );
                sab = new Int8Array(memory.buffer);
              }
              sab[readOffset] = value[i];
            }
          },
          close() {
            console.log('Done writing input stream.');
          },
        })
      ),
      audioReader.read().then(async function process({ value, done }) {
        // avoid clipping start of MediaStreamTrackGenerator output
        if (ac.currentTime < value.buffer.duration * 100) {
          return audioWriter
            .write(value)
            .then(() => audioReader.read().then(process));
        }
        if (writeOffset > readOffset) {
          // avoid clipping end of MediaStreamTrackGenerator output
          if (ac.currentTime < duration + value.buffer.duration * 200) {
            return audioReader.read().then(process);
          } else {
            msd.disconnect();
            osc.disconnect();
            source.disconnect();
            track.stop();
            audioReader.releaseLock();
            await audioReadable.cancel();
            audioWriter.releaseLock();
            generator.stop();
            await ac.close();
            console.log(
              `readOffset: ${readOffset}, writeOffset: ${writeOffset}, duration: ${duration}, ac.currentTime: ${ac.currentTime}`
            , source.mediaStream.getTracks()[0], track, processor, generator);
            return await Promise.all([
              new Promise((resolve) => (stream.oninactive = resolve)),
              new Promise((resolve) => (ac.onstatechange = resolve)),
            ]);
          }
        }
        const { timestamp } = value;
        const int8 = new Int8Array(440);
        const sab = new Int8Array(memory.buffer);
        for (let i = 0; i < 440; i++) {
          int8[i] = sab[writeOffset];
          ++writeOffset;
        }
        const int16 = new Int16Array(int8.buffer);
        const floats = new Float32Array(220);
        for (let i = 0; i < int16.length; i++) {
          const int = int16[i];
          // If the high bit is on, then it is a negative number, and actually counts backwards.
          const float =
            int >= 0x8000 ? -(0x10000 - int) / 0x8000 : int / 0x7fff;
          floats[i] = float;
        }
        const buffer = new AudioBuffer({
          numberOfChannels: 1,
          length: floats.length,
          sampleRate: 22050,
        });
        buffer.copyToChannel(floats, 0, 0);
        duration += buffer.duration;
        const frame = new AudioFrame({ timestamp, buffer });
        return audioWriter.write(frame).then(() => {
          return audioReader.read().then(process);
        });
      }),
    , ac.resume()]);
}).then(console.log, console.error);

I need to be able to do that on any URL, for example, github.com/* where CSP restricts loading URL's from non-github origins.

ilyaigpetrov

unread,
May 8, 2021, 12:37:54 PM5/8/21
to guest271314, Chromium Extensions
Maybe chrome.runtime.sendMessage from a site and chrome.runtime.onMessageExternal inside extension could be used.
My little tests reveal that it doesn't work for some reason but, please, try yourself.

guest271314

unread,
May 8, 2021, 12:52:43 PM5/8/21
to Chromium Extensions, ilyaigpetrov, Chromium Extensions, guest271314
That is the purpose of updating externally_connectable in manifest.json.

I am trying to avoid that approach and use fetch() to request resource on localhost without using sendMessage().

ilyaigpetrov

unread,
May 8, 2021, 1:05:21 PM5/8/21
to guest271314, Chromium Extensions
Can you apply a content script to every site and communicate from it to the extension?
Or from the content script directly to localhost, but I guess CSP rules apply to content scripts too, didn't test it myself though.

> I am trying to avoid that approach and use fetch() to request resource on localhost without using sendMessage().

`fetch` is subjected to CSP while sendMessage shouldn't be.

guest271314

unread,
May 11, 2021, 12:09:36 AM5/11/21
to Chromium Extensions, ilyaigpetrov, Chromium Extensions, guest271314
Ideally chrome.runtime.reload() would be called from console at arbitrary websites. I have not yet found a way to enable that feature.

Currently, to fetch() localhost from any URL at console 

manifest.json

{
  "name": "fetch_locahost",
  "version": "1.0",
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "externally_connectable": {
    "matches": [
    ],
    "ids": [
      "*"
    ]
  },
  "action": {}
}

background.js

chrome.runtime.onConnectExternal.addListener((port) => {
    console.log(port.name, port.sender);
    port.onMessage.addListener(async (message) => {
      console.log(message);
      try {
      const fd = new FormData();
      fd.append('tts', message);
      const readable = (await fetch('http://localhost:8000', {
        method: 'POST',
        body: fd
      })).body;
      readable.pipeTo(
        new WritableStream({
          write(value) {
            port.postMessage(value);
          }, close() {
            port.disconnect();
          }
        })
      );
      } catch (err) {
        console.log(err);
      }
    });
  });
chrome.action.setBadgeText({text:'tts'});
chrome.action.onClicked.addListener(() => {
  chrome.runtime.reload();
});

console

(async(set_externally_connectable = ["https://example.com/*"], unset_externally_connectable = true) => {
  const [fileHandle] = await self.showOpenFilePicker();
  const file = await fileHandle.getFile();
  const manifest_json = await (new Response(file)).json();
  manifest_json.externally_connectable.matches = unset_externally_connectable ? set_externally_connectable :
    [...manifest_json.externally_connectable.matches, ...set_externally_connectable];
  const writer = await fileHandle.createWritable({keepExistingData:false});
  await writer.write(JSON.stringify(manifest_json, null, 2)); 
  return await writer.close();
})([`${location.origin}/*`], false);

Click 'tts' badge to execute chrome.runtime.reload() in extension (lastError can be thrown at website console when service worker is 'inactive', does not reload)

var id = 'flmjmgfajgcdlbfdekihpfmmneaieagc';
var port = chrome.runtime.connect(id);
var {readable: portReadable, writable: portWritable} = new TransformStream();
var portWriter = portWritable.getWriter();
stream(portReadable).then(console.log, console.error);
port.onMessage.addListener(async(e)=>await portWriter.write(new Uint8Array(Object.values(e))));
port.onDisconnect.addListener(async(e)=>await portWriter.close(););
port.postMessage(`espeak-ng -m --stdout -v 'Storm' "test"`);

Reply all
Reply to author
Forward
0 new messages