Compare commits

..

No commits in common. "master" and "v0.2.0" have entirely different histories.

12 changed files with 71 additions and 330 deletions

8
.gitignore vendored
View File

@ -1,9 +1 @@
*.xpi *.xpi
*.swp
# Locally built binaries
/native_application/gorecv
/example_client/gosend
/web-ext-artifacts

120
README.md
View File

@ -1,118 +1,2 @@
# Native Inserter #Clipboard Inserter
A simple addon whose purpose is to automatically insert contents of clipboard into the page
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

View File

@ -1,6 +1,5 @@
<html> <html>
<head> <head>
<meta charset="utf-8"/>
<script src="/default-options.js"></script> <script src="/default-options.js"></script>
<script src="monitor.js"></script> <script src="monitor.js"></script>
</head> </head>

View File

@ -1,13 +1,14 @@
console.log("I'm alive") console.log("I'm alive")
let previousContent = ""
let listeningTabs = [] let listeningTabs = []
let timer = null
let options = defaultOptions let options = defaultOptions
chrome.storage.local.get(defaultOptions, browser.storage.local.get(defaultOptions)
o => options = o) .then(o => options = o)
browser.storage.onChanged.addListener((changes, area) => {
chrome.storage.onChanged.addListener((changes, area) => {
if(area === "local") { if(area === "local") {
const optionKeys = Object.keys(options) const optionKeys = Object.keys(options)
for(key of Object.keys(changes)) { for(key of Object.keys(changes)) {
@ -15,25 +16,13 @@ chrome.storage.onChanged.addListener((changes, area) => {
options[key] = changes[key].newValue options[key] = changes[key].newValue
} }
} }
updateTimer()
} }
}) })
chrome.browserAction.onClicked.addListener((tab) => { browser.browserAction.onClicked.addListener(() => {
toggleTab(tab.id) browser.tabs.query({active: 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))
}) })
function toggleTab(id) { function toggleTab(id) {
@ -41,19 +30,56 @@ function toggleTab(id) {
if(index >= 0) { if(index >= 0) {
uninject(id) uninject(id)
listeningTabs.splice(index, 1) listeningTabs.splice(index, 1)
chrome.browserAction.setBadgeText({ text: "", tabId: id }) updateTimer()
browser.browserAction.setBadgeText({ text: "", tabId: id })
} else { } else {
chrome.tabs.executeScript({file: "/fg/insert.js"}) browser.tabs.executeScript({file: "/fg/insert.js"})
listeningTabs.push(id) listeningTabs.push(id)
chrome.browserAction.setBadgeBackgroundColor({ color: "green", tabId: id }) updateTimer()
chrome.browserAction.setBadgeText({ text: "ON", tabId: id }) browser.browserAction.setBadgeBackgroundColor({ color: "green", tabId: id })
browser.browserAction.setBadgeText({ text: "ON", tabId: id })
} }
} }
function notifyForeground(id, text) { function notifyForeground(id, text) {
chrome.tabs.sendMessage(id, { action: "insert", text, options }) browser.tabs.sendMessage(id, {
action: "insert", text, options
})
} }
function uninject(id) { 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) {
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()
}
} }

View File

@ -1,3 +0,0 @@
module gosend
go 1.17

View File

@ -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)
}
}

View File

@ -7,10 +7,10 @@
document.querySelector(msg.options.containerSelector).appendChild(elem) document.querySelector(msg.options.containerSelector).appendChild(elem)
break break
case "uninject": case "uninject":
chrome.runtime.onMessage.removeListener(processMessage) browser.runtime.onMessage.removeListener(processMessage)
break break
} }
} }
chrome.runtime.onMessage.addListener(processMessage) browser.runtime.onMessage.addListener(processMessage)
})() })()

View File

@ -1,24 +1,16 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Native Inserter", "name": "Clipboard Inserter",
"version": "1.0.0", "version": "0.2.0",
"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": {}, "icons": {},
"browser_specific_settings": {
"gecko": {
"id": "@native-inserter",
"strict_min_version": "50.0"
}
},
"permissions": [ "permissions": [
"activeTab", "activeTab",
"storage", "clipboardRead",
"nativeMessaging", "storage"
"file://*/*"
], ],
"browser_action": { "browser_action": {
@ -28,7 +20,7 @@
"32": "icon/icon32.png", "32": "icon/icon32.png",
"64": "icon/icon64.png" "64": "icon/icon64.png"
}, },
"default_title": "Toggle Native Inserter" "default_title": "Toggle clipboard inserter"
}, },
"background": { "background": {

View File

@ -1,3 +0,0 @@
module gorecv
go 1.17

View File

@ -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
}

View File

@ -1,7 +0,0 @@
{
"name": "native_inserter",
"description": "Example host for native messaging",
"path": "/usr/local/bin/gorecv",
"type": "stdio",
"allowed_extensions": [ "@native-inserter" ]
}

View File

@ -3,9 +3,9 @@ document.addEventListener("DOMContentLoaded", () => {
containerSelector = document.querySelector("#container-selector"), containerSelector = document.querySelector("#container-selector"),
monitorInterval = document.querySelector("#monitor-interval") 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 elemName.value = o.elemName
containerSelector.value = o.containerSelector containerSelector.value = o.containerSelector
monitorInterval.value = o.monitorInterval monitorInterval.value = o.monitorInterval