From 9f5f3b925baf4cea3f35970ca6df749b567596f2 Mon Sep 17 00:00:00 2001 From: roelderickx Date: Sun, 9 Jan 2022 10:27:59 +0100 Subject: [PATCH 1/9] Add function to log in browser console --- nativemessaging/__init__.py | 29 ++------------------------- nativemessaging/nativemessaging.py | 32 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 nativemessaging/nativemessaging.py diff --git a/nativemessaging/__init__.py b/nativemessaging/__init__.py index f393651..c89649b 100644 --- a/nativemessaging/__init__.py +++ b/nativemessaging/__init__.py @@ -1,28 +1,3 @@ -#!/usr/bin/env python -import json -import sys -import struct +# -*- coding: utf-8 -*- - -# from https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging -def get_message(): - raw_length = sys.stdin.buffer.read(4) - if len(raw_length) == 0: - sys.exit(0) - message_length = struct.unpack("@I", raw_length)[0] - message = sys.stdin.buffer.read(message_length).decode("utf-8") - return json.loads(message) - - -# Encode a message for transmission, given its content. -def encode_message(message_content): - encoded_content = json.dumps(message_content).encode("utf-8") - encoded_length = struct.pack("@I", len(encoded_content)) - return {"length": encoded_length, "content": encoded_content} - - -# Send an encoded message to stdout. -def send_message(encoded_message): - sys.stdout.buffer.write(encoded_message["length"]) - sys.stdout.buffer.write(encoded_message["content"]) - sys.stdout.buffer.flush() +from nativemessaging import * diff --git a/nativemessaging/nativemessaging.py b/nativemessaging/nativemessaging.py new file mode 100644 index 0000000..a786527 --- /dev/null +++ b/nativemessaging/nativemessaging.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +import json +import sys +import struct + +def log_browser_console(message): + sys.stderr.write(message + '\n') + + +# from https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging +def get_message(): + raw_length = sys.stdin.buffer.read(4) + if len(raw_length) == 0: + sys.exit(0) + message_length = struct.unpack("@I", raw_length)[0] + message = sys.stdin.buffer.read(message_length).decode("utf-8") + return json.loads(message) + + +# Encode a message for transmission, given its content. +def encode_message(message_content): + encoded_content = json.dumps(message_content).encode("utf-8") + encoded_length = struct.pack("@I", len(encoded_content)) + return {"length": encoded_length, "content": encoded_content} + + +# Send an encoded message to stdout. +def send_message(encoded_message): + sys.stdout.buffer.write(encoded_message["length"]) + sys.stdout.buffer.write(encoded_message["content"]) + sys.stdout.buffer.flush() From 1e094f9458f5fab2d7e10959a277a5622ad01aaf Mon Sep 17 00:00:00 2001 From: roelderickx Date: Sun, 9 Jan 2022 10:32:57 +0100 Subject: [PATCH 2/9] Raise an exception if message is empty in stead of quitting the application --- nativemessaging/nativemessaging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nativemessaging/nativemessaging.py b/nativemessaging/nativemessaging.py index a786527..a30bfc3 100644 --- a/nativemessaging/nativemessaging.py +++ b/nativemessaging/nativemessaging.py @@ -12,7 +12,7 @@ def log_browser_console(message): def get_message(): raw_length = sys.stdin.buffer.read(4) if len(raw_length) == 0: - sys.exit(0) + raise Exception("Message is empty") message_length = struct.unpack("@I", raw_length)[0] message = sys.stdin.buffer.read(message_length).decode("utf-8") return json.loads(message) From 5b6ae5adf3a40be5c81bb3f972a9730d27c37007 Mon Sep 17 00:00:00 2001 From: roelderickx Date: Sun, 9 Jan 2022 10:41:08 +0100 Subject: [PATCH 3/9] Remove functions the user does not need to know about --- README.md | 10 +++------- nativemessaging/nativemessaging.py | 15 +++++---------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9d6e8e5..5095ebe 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,8 @@ Based on [Native Messaging on MDN](https://developer.mozilla.org/en-US/docs/Mozi If [`runtime.connectNative`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connectNative) is used, `get_message()` must be called repeatedly in a loop to poll for messages. If [`runtime.sendNativeMessage`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage) is used, `get_message()` only needs to be called once. -## `encode_message( message_content )` -`nativemessaging.encode_message()` takes one argument, a message to be encoded. -Returns an encoded version of a message to be returned to the browser. Use with `send_message()`. - -## `send_message( encoded_message )` -`nativemessaging.send_message()` takes one argument, an encoded message from `encode_message()`. Returns a message to the browser. +## `send_message( message )` +`nativemessaging.send_message()` takes one argument, a message to be returned to the browser. ## Sample Browser side: @@ -42,7 +38,7 @@ import nativemessaging while True: message = nativemessaging.get_message() if message == "hello": - nativemessaging.send_message(nativemessaging.encode_message("world")) + nativemessaging.send_message("world") ``` ## nativemessaging-install diff --git a/nativemessaging/nativemessaging.py b/nativemessaging/nativemessaging.py index a30bfc3..f5fb85d 100644 --- a/nativemessaging/nativemessaging.py +++ b/nativemessaging/nativemessaging.py @@ -18,15 +18,10 @@ def get_message(): return json.loads(message) -# Encode a message for transmission, given its content. -def encode_message(message_content): - encoded_content = json.dumps(message_content).encode("utf-8") - encoded_length = struct.pack("@I", len(encoded_content)) - return {"length": encoded_length, "content": encoded_content} - - # Send an encoded message to stdout. -def send_message(encoded_message): - sys.stdout.buffer.write(encoded_message["length"]) - sys.stdout.buffer.write(encoded_message["content"]) +def send_message(message): + encoded_content = json.dumps(message).encode("utf-8") + encoded_length = struct.pack("@I", len(encoded_content)) + sys.stdout.buffer.write(encoded_length) + sys.stdout.buffer.write(encoded_content) sys.stdout.buffer.flush() From 5785c665cc2d689585514f4171a158b1964afb76 Mon Sep 17 00:00:00 2001 From: roelderickx Date: Sun, 9 Jan 2022 10:57:38 +0100 Subject: [PATCH 4/9] Add documentation to functions --- nativemessaging/__init__.py | 2 +- nativemessaging/nativemessaging.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/nativemessaging/__init__.py b/nativemessaging/__init__.py index c89649b..b30a7e9 100644 --- a/nativemessaging/__init__.py +++ b/nativemessaging/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from nativemessaging import * +from .nativemessaging import * diff --git a/nativemessaging/nativemessaging.py b/nativemessaging/nativemessaging.py index f5fb85d..79437d9 100644 --- a/nativemessaging/nativemessaging.py +++ b/nativemessaging/nativemessaging.py @@ -5,11 +5,17 @@ import struct def log_browser_console(message): + ''' + Log a message in the browser console + ''' sys.stderr.write(message + '\n') # from https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging def get_message(): + ''' + Receive a native message from the browser + ''' raw_length = sys.stdin.buffer.read(4) if len(raw_length) == 0: raise Exception("Message is empty") @@ -18,8 +24,10 @@ def get_message(): return json.loads(message) -# Send an encoded message to stdout. def send_message(message): + ''' + Send a native message to the browser + ''' encoded_content = json.dumps(message).encode("utf-8") encoded_length = struct.pack("@I", len(encoded_content)) sys.stdout.buffer.write(encoded_length) From bb77cf4b2fd288aefee53d58213e413383681097 Mon Sep 17 00:00:00 2001 From: roelderickx Date: Sun, 9 Jan 2022 10:58:31 +0100 Subject: [PATCH 5/9] Add test script --- test/example_message | Bin 0 -> 41 bytes test/test.py | 11 +++++++++++ test/test.sh | 3 +++ 3 files changed, 14 insertions(+) create mode 100644 test/example_message create mode 100644 test/test.py create mode 100755 test/test.sh diff --git a/test/example_message b/test/example_message new file mode 100644 index 0000000000000000000000000000000000000000..426c4018befdffffc73a8905d4bcf5653effc0a0 GIT binary patch literal 41 ucmY#oU|^_L%1F)0$yc&cP%6(a%1KeuQBW#LEiM5Gq*f&67UZNV)dB$KKnxZD literal 0 HcmV?d00001 diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..8524a56 --- /dev/null +++ b/test/test.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +# -*- coding: utf-8 -*- + +import nativemessaging + +msg = nativemessaging.get_message() +nativemessaging.log_browser_console("received msg: %s" % str(msg)) +nativemessaging.send_message("test") + +print() diff --git a/test/test.sh b/test/test.sh new file mode 100755 index 0000000..1419fda --- /dev/null +++ b/test/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +PYTHONPATH=.. python test.py < example_message From 4b548c1383118622d1b87f569b1d1a0168db3466 Mon Sep 17 00:00:00 2001 From: roelderickx Date: Sun, 9 Jan 2022 12:11:25 +0100 Subject: [PATCH 6/9] Export installation function --- bin/nativemessaging-install.py | 97 ---------------------------- nativemessaging/__init__.py | 1 + nativemessaging/__main__.py | 5 ++ nativemessaging/install.py | 113 +++++++++++++++++++++++++++++++++ setup.py | 4 +- 5 files changed, 122 insertions(+), 98 deletions(-) delete mode 100644 bin/nativemessaging-install.py create mode 100644 nativemessaging/__main__.py create mode 100644 nativemessaging/install.py diff --git a/bin/nativemessaging-install.py b/bin/nativemessaging-install.py deleted file mode 100644 index 34dccd3..0000000 --- a/bin/nativemessaging-install.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os -import sys -import json -if sys.platform == "win32": - import winreg - -# locations for storing manifests -browser_info = { - "chrome": { - "registry": "Software\\Google\\Chrome\\NativeMessagingHosts", - "linux": os.path.join(os.path.expandvars("$HOME"), ".config/google-chrome/NativeMessagingHosts"), - "darwin": os.path.join(os.path.expandvars("$HOME"), "Library/Application Support/Google/Chrome/NativeMessagingHosts") - }, - "firefox": { - "registry": "Software\\Mozilla\\NativeMessagingHosts", - "linux": os.path.join(os.path.expandvars("$HOME"), ".mozilla/native-messaging-hosts"), - "darwin": os.path.join(os.path.expandvars("$HOME"), "Library/Application Support/Mozilla/NativeMessagingHosts") - } -} - - -def options(): - ap = argparse.ArgumentParser() - ap.add_argument("browser", choices=["chrome", "firefox"], nargs="+") - ap.add_argument("--manifest") - return vars(ap.parse_args()) - - -def readFile(file): - with open(file, "r") as f: - return f.read() - - -def writeFile(file, contents): - with open(file, "w") as f: - f.write(contents) - - -def createRegKey(path, value): - winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) - registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_WRITE) - winreg.SetValue(registry_key, "", winreg.REG_SZ, value) - winreg.CloseKey(registry_key) - print("Created registry key at HKEY_CURRENT_USER\\" + path) - - -def writeManifest(browser, path, manifest): - if browser == "firefox": - manifest.pop("allowed_origins", None) - elif browser == "chrome": - manifest.pop("allowed_extensions", None) - writeFile(path, json.dumps(manifest)) - print("Saved manifest file to " + path) - - -def main(): - opts = options() - if opts["manifest"] is not None and os.path.isfile(opts["manifest"]): # read from arguments if available - print("Reading manifest: " + opts["manifest"]) - file = readFile(opts["manifest"]) - elif os.path.isfile("native-manifest.json"): # fall back to native-manifest.json - print("Reading manifest: native-manifest.json") - file = readFile("native-manifest.json") - else: - raise Exception("No manifest found. Supply a manifest in the arguments, or create a manifest named native-manifest.json") - manifest = json.loads(file) - manifest["path"] = os.path.abspath(manifest["path"]) # ensure path is absolute - print("Absolute path: " + manifest["path"]) - if sys.platform == "win32": - install_dir = os.path.dirname(manifest["path"]) - if manifest["path"].endswith(".py"): # create batch file for python apps in windows - batch_path = os.path.join(install_dir, manifest["name"] + ".bat") - writeFile(batch_path, - "@echo off\npython -u \"{0}\"".format(manifest["path"])) - manifest["path"] = batch_path - print("Batch file created at: " + manifest["path"]) - # write registry key on windows - for browser in opts["browser"]: - manifest_path = os.path.join(install_dir, "{0}_{1}.json".format(manifest["name"], browser)) - writeManifest(browser, manifest_path, manifest) - createRegKey(os.path.join(browser_info[browser]["registry"], manifest["name"]), - manifest_path) - if sys.platform in ["linux", "darwin"]: # save manifest in linux and mac - for browser in opts["browser"]: - manifest_path = os.path.join(browser_info[browser][sys.platform], - manifest["name"] + ".json") - manifest_path_folder = os.path.dirname(manifest_path) - if not os.path.exists(manifest_path_folder): - os.mkdir(manifest_path_folder) - writeManifest(browser, manifest_path, manifest) - input("Done! Press enter or close the window.") - - -if __name__ == "__main__": - main() diff --git a/nativemessaging/__init__.py b/nativemessaging/__init__.py index b30a7e9..7ec017d 100644 --- a/nativemessaging/__init__.py +++ b/nativemessaging/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from .nativemessaging import * +from .install import install diff --git a/nativemessaging/__main__.py b/nativemessaging/__main__.py new file mode 100644 index 0000000..86fe291 --- /dev/null +++ b/nativemessaging/__main__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from .install import main + +main() diff --git a/nativemessaging/install.py b/nativemessaging/install.py new file mode 100644 index 0000000..1200bf2 --- /dev/null +++ b/nativemessaging/install.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +import argparse +import os +import sys +import json +if sys.platform == "win32": + import winreg + +# locations for storing manifests +browser_info = { + "chrome": { + "registry": "Software\\Google\\Chrome\\NativeMessagingHosts", + "linux": os.path.join(os.path.expandvars("$HOME"), ".config/google-chrome/NativeMessagingHosts"), + "darwin": os.path.join(os.path.expandvars("$HOME"), "Library/Application Support/Google/Chrome/NativeMessagingHosts") + }, + "firefox": { + "registry": "Software\\Mozilla\\NativeMessagingHosts", + "linux": os.path.join(os.path.expandvars("$HOME"), ".mozilla/native-messaging-hosts"), + "darwin": os.path.join(os.path.expandvars("$HOME"), "Library/Application Support/Mozilla/NativeMessagingHosts") + } +} + + +def write_file(filename, contents): + with open(filename, "w") as f: + f.write(contents) + + +def write_manifest(browser, path, manifest): + if browser == "firefox": + manifest.pop("allowed_origins", None) + elif browser == "chrome": + manifest.pop("allowed_extensions", None) + write_file(path, json.dumps(manifest)) + #print("Saved manifest file to " + path) + + +def create_reg_key(path, value): + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_WRITE) + winreg.SetValue(registry_key, "", winreg.REG_SZ, value) + winreg.CloseKey(registry_key) + print("Created registry key at HKEY_CURRENT_USER\\" + path) + + +def install_windows(browsers, manifest): + install_dir = os.path.dirname(manifest["path"]) + if manifest["path"].endswith(".py"): + # create batch file for python apps in windows + batch_path = os.path.join(install_dir, manifest["name"] + ".bat") + write_file(batch_path, "@echo off\npython -u \"{0}\"".format(manifest["path"])) + manifest["path"] = batch_path + #print("Batch file created at: " + manifest["path"]) + # write registry key on windows + for browser in browsers: + manifest_path = os.path.join(install_dir, "{0}_{1}.json".format(manifest["name"], browser)) + write_manifest(browser, manifest_path, manifest) + create_reg_key(os.path.join(browser_info[browser]["registry"], manifest["name"]), + manifest_path) + + +def install_unix(browsers, manifest): + for browser in browsers: + manifest_path_folder = browser_info[browser][sys.platform] + if not os.path.exists(manifest_path_folder): + os.mkdir(manifest_path_folder) + manifest_path = os.path.join(manifest_path_folder, manifest["name"] + ".json") + write_manifest(browser, manifest_path, manifest) + + +def install(browsers, manifest_file): + # read contents of manifest file + manifest_contents = None + with open(manifest_file, "r") as f: + manifest_contents = f.read() + manifest = json.loads(manifest_contents) + + # ensure path is absolute + manifest["path"] = os.path.abspath(manifest["path"]) + #print("Absolute path: " + manifest["path"]) + + if sys.platform == "win32": + install_windows(browsers, manifest) + elif sys.platform in ["linux", "darwin"]: + install_unix(browsers, manifest) + + +def options(): + ap = argparse.ArgumentParser() + ap.add_argument("browser", choices=["chrome", "firefox"], nargs="+") + ap.add_argument("--manifest") + return vars(ap.parse_args()) + + +def main(): + opts = options() + + manifest_file = "native-manifest.json" + if opts["manifest"] is not None and os.path.isfile(opts["manifest"]): + # read from arguments if available + manifest_file = opts["manifest"] + elif os.path.isfile("native-manifest.json"): + # fall back to native-manifest.json + print("Reading manifest: native-manifest.json") + else: + raise Exception("No manifest found. Supply a manifest in the arguments, or create a manifest named native-manifest.json") + + print("Reading manifest: " + manifest_file) + + install(opts["browser"], manifest_file) + + input("Done! Press enter or close the window.") diff --git a/setup.py b/setup.py index 40089da..c371c6a 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,9 @@ author_email="rayquaza01@outlook.com", license="MPL 2.0", packages=["nativemessaging"], - scripts=["bin/nativemessaging-install.py"], + entry_points={ + "console_scripts": ["nativemessaging-install = nativemessaging.install:main"] + }, include_package_data=True, package_data={"": ["README.md"]}, classifiers=[ From 1eca10216822bc7fd1221cfb2e95a16518fdd7ff Mon Sep 17 00:00:00 2001 From: roelderickx Date: Sun, 9 Jan 2022 12:18:24 +0100 Subject: [PATCH 7/9] Add documentation for install() --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5095ebe..7e2d114 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ If [`runtime.sendNativeMessage`](https://developer.mozilla.org/en-US/docs/Mozill ## `send_message( message )` `nativemessaging.send_message()` takes one argument, a message to be returned to the browser. +## `install( browsers, manifest_file )` +`nativemessaging.install()` takes two arguments, a list of browsers to install the manifest and a manifest filename. Supported browsers are 'chrome' and 'firefox'. + ## Sample Browser side: ```javascript From 5e9ea76c335de191250f5d1dc5491e40b538b7d9 Mon Sep 17 00:00:00 2001 From: roelderickx Date: Sun, 9 Jan 2022 12:26:53 +0100 Subject: [PATCH 8/9] Remove duplicate output message --- nativemessaging/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nativemessaging/install.py b/nativemessaging/install.py index 1200bf2..cd2e15f 100644 --- a/nativemessaging/install.py +++ b/nativemessaging/install.py @@ -102,7 +102,7 @@ def main(): manifest_file = opts["manifest"] elif os.path.isfile("native-manifest.json"): # fall back to native-manifest.json - print("Reading manifest: native-manifest.json") + pass else: raise Exception("No manifest found. Supply a manifest in the arguments, or create a manifest named native-manifest.json") From 9a06711216bbac7d1440df96258876af496fccad Mon Sep 17 00:00:00 2001 From: roelderickx Date: Sun, 9 Jan 2022 12:38:12 +0100 Subject: [PATCH 9/9] Accept manifest contents in stead of manifest file for installation --- README.md | 2 +- nativemessaging/install.py | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7e2d114..35b8507 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ If [`runtime.sendNativeMessage`](https://developer.mozilla.org/en-US/docs/Mozill `nativemessaging.send_message()` takes one argument, a message to be returned to the browser. ## `install( browsers, manifest_file )` -`nativemessaging.install()` takes two arguments, a list of browsers to install the manifest and a manifest filename. Supported browsers are 'chrome' and 'firefox'. +`nativemessaging.install()` takes two arguments, a list of browsers to install the manifest and a manifest. Supported browsers are 'chrome' and 'firefox' and the manifest must be the contents of a valid manifest file. ## Sample Browser side: diff --git a/nativemessaging/install.py b/nativemessaging/install.py index cd2e15f..ba1bcdf 100644 --- a/nativemessaging/install.py +++ b/nativemessaging/install.py @@ -21,6 +21,10 @@ } } +def read_file(filename): + with open(filename, "r") as f: + return f.read() + def write_file(filename, contents): with open(filename, "w") as f: @@ -69,13 +73,7 @@ def install_unix(browsers, manifest): write_manifest(browser, manifest_path, manifest) -def install(browsers, manifest_file): - # read contents of manifest file - manifest_contents = None - with open(manifest_file, "r") as f: - manifest_contents = f.read() - manifest = json.loads(manifest_contents) - +def install(browsers, manifest): # ensure path is absolute manifest["path"] = os.path.abspath(manifest["path"]) #print("Absolute path: " + manifest["path"]) @@ -106,8 +104,11 @@ def main(): else: raise Exception("No manifest found. Supply a manifest in the arguments, or create a manifest named native-manifest.json") + # read contents of manifest file print("Reading manifest: " + manifest_file) + manifest_contents = read_file(manifest_file) + manifest = json.loads(manifest_contents) - install(opts["browser"], manifest_file) + install(opts["browser"], manifest) input("Done! Press enter or close the window.")