webext-native-messaging
Simple WebExtension demonstrating native messaging API
This repository contains a simple extension and source code for a native application to test WebExtension Native Messaging API.
There's three parts in the system:
names
The extension that you can install to your browser (only Firefox probably). This sends and receives messages from the native application. This lives in the "extension" folder.
rsio
A rust application that sends and receives messages from whatever launched it. Source code for rsio is in "application" folder.
executable-folder
You would put the rsio binary here.
- rsio.conf is a configuration file that rsio.exe reads on startup
- rsio.json is the manifest file that Firefox reads to get some info about the native application it connects to.
So how does it work?
MDN article about native messaging is here, but here's the main points:
Connect to native application
The extension requests to connect to a native application. Only a background-script is allowed to do this.
browser.runtime.connectNative("rsio");
This tells Firefox to go look for a OS specific location for a info about what this "rsio" application is. This lookup depends on your OS. Full MDN documentation is here
Windows
On Windows, Firefox checks a registry key:
HKEY_CURRENT_USER\SOFTWARE\Mozilla\NativeMessagingHosts\rsio
The value of that registry key must be an absolute file path to rsio.json
Linux
On Linux, Firefox reads per user json file at:
~/.mozilla/native-messaging-hosts/rsio.json
Or system-wide install:
/usr/lib/mozilla/native-messaging-hosts/rsio.json
native application manifest
Next, Firefox tries to read that rsio.json
file. Among other things, the json describes where the actual binary file is. For example:
{
"name": "rsio",
"description": "My test module",
"type": "stdio",
"path": "rsio.exe",
"allowed_extensions": ["names@example.org"]
}
The name field might need to be the same as the json file name. The example would make Firefox lauch rsio.exe
from the same path where the json file was found. Note that Firefox will launch the executable, not connect to an already running instance of the application.
On Linux the path
-key must be absolute path to the executable.
The allowed_extensions field just tells that the native application wwill only receive connection from an extension of that specific ID.
If all went well, Firefox will execute the binary.
Communication
Messages are passed using standard IO, and are composed of 32bit "header" telling the length of the message, followed by the message payload in valid JSON form. If Firefox receives standard IO that doesn't follow this format, then it will kill the application.
Extension code doesn't need to worry about any of that. It can just send a string and let Firefox worry about any encoding and/decoding.
That 32bit "header" means that your native application cannot must do some encoding/decoding to pass anything to Firefox. It also means you should probably use some library to handle JSON formatted data.
rsio
doesn't include any JSON library though, so there's a good chance you can crash it by sending some sort of data. Have a go and see what happens.
Extension
The names
extension adds a button to toolbar that, when clicked gets the current tab url from the browser and sends it to rsio
.
port.postMessage("mirror " + tab.url)
The mirror
command tells rsio to send the decoded message back to the other browser. If clicked on all went well this should log "mirror " to the console of that extension. NOT the active web content console, you have to use about:debugging
to read it.
What commands does rsio support?
ping
>> port.postMessage("ping")
<< "pong"
mirror
>> port.postMessage("mirror mirror on the wall")
<< "mirror mirror on the wall"
quit
Closes rsio
, extension code receives a ondisconnected
event.
>> port.postMessage("quit")
<<
config
Returns the parsed rsio.conf contents
>> port.postMessage("config")
<< name=MrOtherGuy
<< exec_path=C:\test\rsio_executables
write
Writes text to a file. myt.txt would be created in the same folder as rsio binary.
>> port.postMessage("write myt.txt This should get written to myt.txt file right here")
<< wrote file: myt.txt
dostuff
Tries to execute whatever you pass it to.
>> port.postMessage("dostuff firefox.exe https://wikipedia.org -p test")
<< Doing stuff: C:\test\rsio_executables\firefox.exe
Well, that assumes you have a firefox.exe
file in C:\test\rsio_executables\
which you probably don't.
rsio first tries to find a file that matches the second part of the message i.e. <path_to_rsio.exe>/firefox.exe
but if that is not a file / doesn't exist etc. Then it tries to use the exec_path
defined in rsio.conf
as base.
You can do something like this too:
>> port.postMessage("dostuff bin/firefox.exe https://wikipedia.org -p test")
<< Doing stuff: bin/putty.exe
That would run putty.exe if it is found in <path_to_rsio.exe>/bin/putty.exe
If such file doesn't exist, then rsio tries to launch it from the exec_path
again.
rsio ``dostuff` executes the program as a child process. Thus if rsio terminates, the child process will likely be killed by your OS. Also, if the program you lauch just waits for input before doing anything, then rsio will probably just lock up indefinitely and not respond to any messages from the extension. So, don't launch programs that do that. An example would be:
>> port.postMessage("dostuff rsio.exe")
A whole bunch of nothing will happen, but rsio will not respond to anything because it waits for response from the child-rsio which itself waits for input from the parent-rsio.
There isn't currently any way to forward the output of that third-party program launched by rsio back into the extension.
Compiling rsio
cd
into application
folder and do:
cargo build --target=x86_64-pc-windows-gnu
On Linux a simple should work.
cargo build
Tried on Debian runnnig in WSL, and also stand-alone Fedora 34. No promises how it goes on other platforms.
After you have compiled it successfully, just copy the executable to whereever your rsio.json
is located.
Note, you can close the old instance from Firefox using the "quit" command after which you can replace the old binary.
What do I do with this?
I dunno, use your imagination. It's just a learning exercise.