Compare commits

..

19 Commits

Author SHA1 Message Date
45Tatami d7d53e8cbc Clean/Fixup Readme 2022-01-04 21:28:50 +01:00
45Tatami 106361363d Update gitignore 2022-01-01 12:43:22 +01:00
45Tatami 9410140cad Add example client 2022-01-01 12:43:21 +01:00
45Tatami 713c2b0f04 Add native application 2022-01-01 12:43:20 +01:00
45Tatami 61c0a11524 Fork 2022-01-01 12:43:16 +01:00
unknown 924a8f1350 Respect newlines in clipboard text 2018-12-06 17:41:54 +01:00
Kamil Tomala 7930e94f85 Bump the version to 0.3.2 2018-12-06 17:31:30 +01:00
Kamil Tomala a5f12daa3c Update the readme 2018-10-10 17:40:27 +02:00
rainyeveryday 88eead2285 Fixes #2, file:// URLs in Chrome 2018-07-07 14:34:21 +02:00
Kamil Tomala 1a86c434eb Bump the version to 0.3.1 2018-07-07 14:32:38 +02:00
Kamil Tomala 50dcc2191c Make the description shorter for chrome web store 2018-06-06 17:04:43 +02:00
Kamil Tomala 8d3a4ca588 Bump the version to 0.3.0 2018-06-03 13:52:14 +02:00
Kamil Tomala dea8c293aa Fix lines starting with space getting inserted twice 2018-06-03 13:50:06 +02:00
Kamil Tomala f33e5642c5
Merge pull request #1 from rainyeveryday/master
Update to work with Chrome
2018-06-03 13:22:37 +02:00
rainyeveryday 26d9434b28 Update to work with Chrome 2018-06-02 20:25:20 -04:00
Kamil Tomala e977862e44 Fix slowdown when clipboard contains pictures 2017-08-13 18:19:36 +02:00
Kamil Tomala f5005a877e Fix the toggle button toggling in a tab of the wrong window 2017-08-13 16:47:02 +02:00
Kamil Tomala 2a4911d827 Add an explicit addon id to the manifest
Fixes lint warnings and (hopefully) the missing browser.runtime in
content scripts when running via web-ext run
2017-08-13 16:23:07 +02:00
Kamil Tomala 5aff71826b Bump the version to 0.2.1 2017-08-13 15:15:26 +02:00
12 changed files with 330 additions and 71 deletions

8
.gitignore vendored
View File

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

120
README.md
View File

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

View File

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

View File

@ -1,14 +1,13 @@
console.log("I'm alive")
let previousContent = ""
let listeningTabs = []
let timer = null
let options = defaultOptions
browser.storage.local.get(defaultOptions)
.then(o => options = o)
chrome.storage.local.get(defaultOptions,
o => options = o)
browser.storage.onChanged.addListener((changes, area) => {
chrome.storage.onChanged.addListener((changes, area) => {
if(area === "local") {
const optionKeys = Object.keys(options)
for(key of Object.keys(changes)) {
@ -16,70 +15,45 @@ browser.storage.onChanged.addListener((changes, area) => {
options[key] = changes[key].newValue
}
}
updateTimer()
}
})
browser.browserAction.onClicked.addListener(() => {
browser.tabs.query({active: true})
.then(([t]) => toggleTab(t.id))
chrome.browserAction.onClicked.addListener((tab) => {
toggleTab(tab.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) {
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 })
}
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 })
}
}
function notifyForeground(id, text) {
browser.tabs.sendMessage(id, {
action: "insert", text, options
})
chrome.tabs.sendMessage(id, { action: "insert", text, options })
}
function uninject(id) {
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()
}
chrome.tabs.sendMessage(id, { action: "uninject" })
}

3
example_client/go.mod Normal file
View File

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

46
example_client/main.go Normal file
View File

@ -0,0 +1,46 @@
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)
break
case "uninject":
browser.runtime.onMessage.removeListener(processMessage)
chrome.runtime.onMessage.removeListener(processMessage)
break
}
}
browser.runtime.onMessage.addListener(processMessage)
chrome.runtime.onMessage.addListener(processMessage)
})()

View File

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

View File

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

View File

@ -0,0 +1,93 @@
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
}

7
native_inserter.json Normal file
View File

@ -0,0 +1,7 @@
{
"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"),
monitorInterval = document.querySelector("#monitor-interval")
const storage = browser.storage.local
const storage = chrome.storage.local
storage.get(defaultOptions).then(o => {
storage.get(defaultOptions, o => {
elemName.value = o.elemName
containerSelector.value = o.containerSelector
monitorInterval.value = o.monitorInterval