Compare commits
No commits in common. "master" and "v0.2.1" have entirely different histories.
|
|
@ -1,9 +1 @@
|
|||
*.xpi
|
||||
|
||||
*.swp
|
||||
|
||||
# Locally built binaries
|
||||
/native_application/gorecv
|
||||
/example_client/gosend
|
||||
|
||||
/web-ext-artifacts
|
||||
|
|
|
|||
120
README.md
120
README.md
|
|
@ -1,118 +1,2 @@
|
|||
# Native Inserter
|
||||
|
||||
Fork of the Clipboard Inserter whose purpose it is to automatically insert the
|
||||
data received by a native application into a broswer page.
|
||||
|
||||
It is useful for use with text extraction tools like Textractor for looking up
|
||||
words in the browser.
|
||||
|
||||
This repository contains the browser plugin, native application, native
|
||||
messaging manifest and an example client. Textractor plugin is not included.
|
||||
|
||||
Currently this addon is not available on any of the browsers addon stores, nor
|
||||
are releases past the fork signed by our benevolent overlords at
|
||||
Mozilla/Google.
|
||||
|
||||
Only tested on GNU/Linux with Firefox 91 ESR.
|
||||
|
||||
## Install
|
||||
|
||||
You will need:
|
||||
|
||||
- This code
|
||||
- A go compiler
|
||||
- A browser supporting native extensions (eg modern Firefox, Chrome)
|
||||
|
||||
1) Compile and install the native application
|
||||
2) Edit `native_inserter.json` to find the native application
|
||||
3) Put `native_inserter.json` into your browsers native messaging manifest directory
|
||||
4) Build and install the addon in your browser
|
||||
|
||||
For this to be useful you will also need a sender, for example a [Textractor
|
||||
plugin](https://github.com/45Tatami/Textractor-TCPSender) and a HTML page ([for
|
||||
example](https://pastebin.com/raw/DRDE075L)) that the can be inserted into.
|
||||
|
||||
### Firefox
|
||||
|
||||
This addon is not signed. From the [official Firefox
|
||||
documentation](https://extensionworkshop.com/documentation/publish/signing-and-distribution-overview/):
|
||||
|
||||
> Unsigned extensions can be installed in Developer Edition, Nightly, and ESR
|
||||
> versions of Firefox, after toggling the xpinstall.signatures.required
|
||||
> preference in about:config.
|
||||
|
||||
You can download the unsigned build from the releases or follow the official
|
||||
instruction for building addons, for example via `web-ext`:
|
||||
|
||||
```
|
||||
$ npm install -g web-ext
|
||||
$ web-ext build
|
||||
```
|
||||
|
||||
You can then install the resulting zip via the `Install Add-On From File`
|
||||
dialog under `about:addons`.
|
||||
|
||||
The native messaging manifest directory under linux is
|
||||
`~/.mozilla/native-messaging-hosts`.
|
||||
|
||||
## How does it work
|
||||
|
||||
The browser plugin starts a native application which creates a raw TCP listen
|
||||
socket currently on all interfaces on port 30501 for incoming connections.
|
||||
|
||||
One or more applications (eg Textractor plugin) will connect to this socket and
|
||||
send messages. Messages consist of a 4 Byte little-endian length header and
|
||||
UTF-8 payload.
|
||||
|
||||
The native application will forward the UTF-8 data to the browser plugin which
|
||||
in turn will insert the text into a webpage similar to the original clipboard
|
||||
version.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
The native application is writing to stderr. Browsers usually forward this to
|
||||
their default log.
|
||||
|
||||
The browser addon itself will log to the console. Problems with the native
|
||||
messaging manifest might not necessarily be logged.
|
||||
|
||||
If the native application does not run in the background after loading the
|
||||
add-on, double-check if the native messaging manifest is in the correct
|
||||
directory and has the binary path set correctly.
|
||||
|
||||
## (Not so) FAQ
|
||||
|
||||
#### Why not the clipboard
|
||||
|
||||
Abusing the clipboard functionality to transfer data to the browser is ugly. It
|
||||
does also not work under Wayland which does not allow unfocused applications to
|
||||
access the clipboard.
|
||||
|
||||
(No opinion here whether the native application/messaging setup is more or less
|
||||
ugly than the clipboard approach)
|
||||
|
||||
It also has the bonus of working with VMs without setting up clipboard
|
||||
forwarding between host and guest.
|
||||
|
||||
#### Why a native application
|
||||
|
||||
The APIs including TCP listen sockets are no longer supported by modern
|
||||
Browsers. The only alternative would be connecting to a WebSocket server, but
|
||||
this has two drawbacks:
|
||||
|
||||
1) The API only supports the client side protocol and does not implement a
|
||||
server. This would mean the sending side would need to be the server and the
|
||||
native application the client, which would not work with multiple senders.
|
||||
|
||||
2) The sending side (eg C++ Textractor plugin) would need to implement a
|
||||
Websocket server which is a lot more work than raw tcp.
|
||||
|
||||
## Bugs
|
||||
|
||||
- Native messaging expects the length prefix in native byte order. Go does
|
||||
neither have a non-unsafe way to convert an integer to a byte stream in
|
||||
native order for writing nor a way to detect endianness. The native
|
||||
application will write in little-endian, which will break the protocol on
|
||||
big-endian machines
|
||||
- Messages over the native-messaging 1MB limit will be discarded instead of
|
||||
split up
|
||||
#Clipboard Inserter
|
||||
A simple addon whose purpose is to automatically insert contents of clipboard into the page
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<script src="/default-options.js"></script>
|
||||
<script src="monitor.js"></script>
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
console.log("I'm alive")
|
||||
|
||||
let previousContent = ""
|
||||
let listeningTabs = []
|
||||
let timer = null
|
||||
let options = defaultOptions
|
||||
|
||||
chrome.storage.local.get(defaultOptions,
|
||||
o => options = o)
|
||||
browser.storage.local.get(defaultOptions)
|
||||
.then(o => options = o)
|
||||
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
browser.storage.onChanged.addListener((changes, area) => {
|
||||
if(area === "local") {
|
||||
const optionKeys = Object.keys(options)
|
||||
for(key of Object.keys(changes)) {
|
||||
|
|
@ -15,45 +16,78 @@ chrome.storage.onChanged.addListener((changes, area) => {
|
|||
options[key] = changes[key].newValue
|
||||
}
|
||||
}
|
||||
updateTimer()
|
||||
}
|
||||
})
|
||||
|
||||
chrome.browserAction.onClicked.addListener((tab) => {
|
||||
toggleTab(tab.id)
|
||||
browser.browserAction.onClicked.addListener(() => {
|
||||
browser.tabs.query({ active: true, currentWindow: true })
|
||||
.then(([t]) => toggleTab(t.id))
|
||||
})
|
||||
|
||||
napp = browser.runtime.connectNative("native_inserter")
|
||||
napp.onDisconnect.addListener((p) => {
|
||||
if (p.error) {
|
||||
console.error(`Disconnected native app due to an error: ${p.error.message}`)
|
||||
}
|
||||
})
|
||||
napp.onMessage.addListener((msg) => {
|
||||
console.log("Received: " + msg.body)
|
||||
const pasteTarget = document.querySelector("#paste-target")
|
||||
pasteTarget.innerText = msg.body
|
||||
const content = pasteTarget.innerText
|
||||
listeningTabs.forEach(id => notifyForeground(id, content))
|
||||
})
|
||||
window.onload = () => {
|
||||
document.querySelector("#paste-target").addEventListener("paste", e => {
|
||||
if(e.clipboardData.getData("text/plain") === "") {
|
||||
e.preventDefault() // prevent anything that is not representable as plain text from being pasted
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function toggleTab(id) {
|
||||
const index = listeningTabs.indexOf(id)
|
||||
if(index >= 0) {
|
||||
uninject(id)
|
||||
listeningTabs.splice(index, 1)
|
||||
chrome.browserAction.setBadgeText({ text: "", tabId: id })
|
||||
} else {
|
||||
chrome.tabs.executeScript({file: "/fg/insert.js"})
|
||||
listeningTabs.push(id)
|
||||
chrome.browserAction.setBadgeBackgroundColor({ color: "green", tabId: id })
|
||||
chrome.browserAction.setBadgeText({ text: "ON", tabId: id })
|
||||
}
|
||||
const index = listeningTabs.indexOf(id)
|
||||
if(index >= 0) {
|
||||
uninject(id)
|
||||
listeningTabs.splice(index, 1)
|
||||
updateTimer()
|
||||
browser.browserAction.setBadgeText({ text: "", tabId: id })
|
||||
} else {
|
||||
browser.tabs.executeScript({file: "/fg/insert.js"})
|
||||
listeningTabs.push(id)
|
||||
updateTimer()
|
||||
browser.browserAction.setBadgeBackgroundColor({ color: "green", tabId: id })
|
||||
browser.browserAction.setBadgeText({ text: "ON", tabId: id })
|
||||
}
|
||||
}
|
||||
|
||||
function notifyForeground(id, text) {
|
||||
chrome.tabs.sendMessage(id, { action: "insert", text, options })
|
||||
browser.tabs.sendMessage(id, {
|
||||
action: "insert", text, options
|
||||
})
|
||||
}
|
||||
|
||||
function uninject(id) {
|
||||
chrome.tabs.sendMessage(id, { action: "uninject" })
|
||||
browser.tabs.sendMessage(id, { action: "uninject" })
|
||||
}
|
||||
|
||||
function checkClipboard() {
|
||||
const pasteTarget = document.querySelector("#paste-target")
|
||||
pasteTarget.textContent = ""
|
||||
pasteTarget.focus()
|
||||
document.execCommand("paste")
|
||||
const content = pasteTarget.textContent
|
||||
if(content != previousContent && content != "") {
|
||||
listeningTabs.forEach(id => notifyForeground(id, content))
|
||||
previousContent = content
|
||||
}
|
||||
}
|
||||
|
||||
function updateTimer() {
|
||||
function stop() {
|
||||
clearInterval(timer.id)
|
||||
timer = null
|
||||
}
|
||||
function start() {
|
||||
const id = setInterval(checkClipboard, options.monitorInterval)
|
||||
timer = { id, interval: options.monitorInterval }
|
||||
}
|
||||
if(listeningTabs.length > 0) {
|
||||
if(timer === null) {
|
||||
start()
|
||||
} else if(timer.interval !== options.monitorInterval) {
|
||||
stop()
|
||||
start()
|
||||
}
|
||||
} else {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
module gosend
|
||||
|
||||
go 1.17
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
const remote_addr = "localhost:30501"
|
||||
|
||||
func main() {
|
||||
c, err := net.Dial("tcp", remote_addr)
|
||||
if err != nil {
|
||||
log.Panic("Error connecting:", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
log.Printf("Connected to '%s'. Please input lines\n", remote_addr)
|
||||
|
||||
s := bufio.NewScanner(os.Stdin)
|
||||
for s.Scan() {
|
||||
msg := []byte(s.Text())
|
||||
|
||||
msg_len := len(msg)
|
||||
if msg_len > math.MaxUint32 {
|
||||
log.Println("Message too long")
|
||||
continue
|
||||
}
|
||||
|
||||
len_buf := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(len_buf, uint32(msg_len))
|
||||
|
||||
if _, err := c.Write(len_buf); err != nil {
|
||||
log.Panic("Error sending len:", err)
|
||||
}
|
||||
if _, err := c.Write(msg); err != nil {
|
||||
log.Panic("Error sending payload:", err)
|
||||
}
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
log.Println("Error reading:", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,10 @@
|
|||
document.querySelector(msg.options.containerSelector).appendChild(elem)
|
||||
break
|
||||
case "uninject":
|
||||
chrome.runtime.onMessage.removeListener(processMessage)
|
||||
browser.runtime.onMessage.removeListener(processMessage)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(processMessage)
|
||||
browser.runtime.onMessage.addListener(processMessage)
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Native Inserter",
|
||||
"version": "1.0.0",
|
||||
"name": "Clipboard Inserter",
|
||||
"version": "0.2.1",
|
||||
|
||||
"description": "An addon that inserts contents received from a native application into a page. Forked from clipboard-inserter. Uses icon made by Google from www.flaticon.com licensed by CC 3.0 BY",
|
||||
"description": "A simple addon whose purpose is to automatically insert contents of clipboard into the page. Uses icon made by Google from www.flaticon.com licensed by CC 3.0 BY",
|
||||
|
||||
"icons": {},
|
||||
|
||||
"browser_specific_settings": {
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"id": "@native-inserter",
|
||||
"strict_min_version": "50.0"
|
||||
"id": "@clipboard-inserter"
|
||||
}
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"nativeMessaging",
|
||||
"file://*/*"
|
||||
"clipboardRead",
|
||||
"storage"
|
||||
],
|
||||
|
||||
"browser_action": {
|
||||
|
|
@ -28,7 +26,7 @@
|
|||
"32": "icon/icon32.png",
|
||||
"64": "icon/icon64.png"
|
||||
},
|
||||
"default_title": "Toggle Native Inserter"
|
||||
"default_title": "Toggle clipboard inserter"
|
||||
},
|
||||
|
||||
"background": {
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
module gorecv
|
||||
|
||||
go 1.17
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
const MSG_SIZE_LIMIT = 1000 * 1000 // Unclear if 1MiB or 1MB, using lower bound
|
||||
const LISTEN_INTERFACE = ":30501"
|
||||
|
||||
func main() {
|
||||
log.Println("Listening on", LISTEN_INTERFACE)
|
||||
|
||||
l, err := net.Listen("tcp", LISTEN_INTERFACE)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
msg_pipe := make(chan string)
|
||||
|
||||
// Output routine
|
||||
go func() {
|
||||
for {
|
||||
msg, err := json.Marshal(Message{<-msg_pipe})
|
||||
if err != nil {
|
||||
log.Println("Error encoding message:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
msg_len := len(msg)
|
||||
if msg_len >= MSG_SIZE_LIMIT {
|
||||
log.Println("Message over webextension limit. Discarding")
|
||||
continue
|
||||
}
|
||||
|
||||
len_buf := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(len_buf, uint32(msg_len))
|
||||
|
||||
log.Println(len_buf, string(msg))
|
||||
fmt.Print(string(len_buf))
|
||||
fmt.Print(string(msg))
|
||||
}
|
||||
}()
|
||||
|
||||
// Accept connections
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
log.Println("Error accepting conn:", err)
|
||||
}
|
||||
go conn_hndlr(conn, msg_pipe)
|
||||
}
|
||||
}
|
||||
|
||||
func conn_hndlr(c net.Conn, ch chan<- string) {
|
||||
defer c.Close()
|
||||
log.Println("Connection opened:", c)
|
||||
for {
|
||||
msg, err := read_msg(c)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
ch <- msg
|
||||
}
|
||||
log.Println("Closing connection:", c)
|
||||
}
|
||||
|
||||
func read_msg(c net.Conn) (string, error) {
|
||||
len_buf := make([]byte, 4)
|
||||
|
||||
if _, err := io.ReadFull(c, len_buf); err != nil {
|
||||
log.Println("Len read error:", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
msg_len := binary.LittleEndian.Uint32(len_buf)
|
||||
msg_buf := make([]byte, msg_len)
|
||||
if _, err := io.ReadFull(c, msg_buf); err != nil {
|
||||
log.Println("Msg read error:", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(msg_buf), nil
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"name": "native_inserter",
|
||||
"description": "Example host for native messaging",
|
||||
"path": "/usr/local/bin/gorecv",
|
||||
"type": "stdio",
|
||||
"allowed_extensions": [ "@native-inserter" ]
|
||||
}
|
||||
|
|
@ -3,9 +3,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
containerSelector = document.querySelector("#container-selector"),
|
||||
monitorInterval = document.querySelector("#monitor-interval")
|
||||
|
||||
const storage = chrome.storage.local
|
||||
const storage = browser.storage.local
|
||||
|
||||
storage.get(defaultOptions, o => {
|
||||
storage.get(defaultOptions).then(o => {
|
||||
elemName.value = o.elemName
|
||||
containerSelector.value = o.containerSelector
|
||||
monitorInterval.value = o.monitorInterval
|
||||
|
|
|
|||
Loading…
Reference in New Issue