├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── app ├── main.html ├── main.js └── manifest.json └── native-host ├── config └── com.sample.native_msg_golang.json └── src └── main.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin/ 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | # Log files 20 | *.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 John Farley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chrome Native Messaging in Go 2 | 3 | Simple Chrome browser extension with a native messaging host written in Go. The focus of this extension and 4 | native messaging host is to showcase the creation of a persistent connection using connectNative() to a native messaging host written in Go and exchange JSON formatted messages. 5 | 6 | ## Getting Started 7 | 8 | The project consists of a Chrome extension app and native messaging host. The native messaging host was written in Go. 9 | 10 | ### Prerequisites 11 | 12 | Chrome v74+ 13 | 14 | Go v1.10+ 15 | 16 | Windows 10 17 | 18 | ### Installing 19 | 20 | There are a few steps you must complete to install an unpacked Chrome extension. 21 | 22 | **Step 1**: Build the native messaging host exe. Open a terminal and navigate to 23 | the "*native-host/src directory*" in the project. Then, enter the following 24 | command and hit enter: 25 | 26 | ``` 27 | go build -o bin/nativeMsgSample.exe 28 | ``` 29 | 30 | **Step 2**: Update the `/native-host/config/com.sample.native_msg_golang.json` file. Add the full file path of the *nativeMsgSample.exe* file you just created in step 1 to the "path" property value in the JSON file. 31 | 32 | Example (change this path to match your file path)... 33 | ``` 34 | { 35 | ... 36 | "path": "C:\\code\\github.com\\chrome-native-messaging-golang\\native-host\\src\\bin\\nativeMsgSample.exe", 37 | ... 38 | } 39 | ``` 40 | 41 | **Step 3**: Add required registry key to HKCU. Open the Windows Registry Editor (regedit) and navigate to the following path... 42 | ``` 43 | HKEY_CURRENT_USER/Software/Google/Chrome/NativeMessagingHosts 44 | ``` 45 | - 3.1: Add a new key with title of `com.sample.native_msg_golang` under the *NativeMessagingHosts* key. 46 | - 3.2: After creating the `com.sample.native_msg_golang` key, there should be a "*(Default)*" string value within the key. Right click on that string value and choose "*Modify*". Then, enter the full path to `/native-host/config/com.sample.native_msg_golang.json`. 47 | 48 | **Step 4**: Install the Chrome extension app. 49 | 50 | - 4.1: In Chrome, navigate to `chrome://extensions`. 51 | - 4.2: Enable developer mode by toggling the switch in the upper-right corner. 52 | - 4.3: Click on the "Load unpacked" button. 53 | - 4.4: Select the *app* directory in the project to load the html, js, and json files that make up the unpacked extension. 54 | 55 | **Step 5**: Run the extension. Open a new tab, and click on the *Apps* button in the Chrome browser toolbar or navigate to `chrome://apps`. Find the "*Chrome Native Messaging Go Example*" app and click on it. 56 | 57 | You should see a simple UI containing a button that says "*Connect to Native host*". Click that button to establish a connection to the native messaging host. 58 | 59 | Once connected to the native messaging host, a text box and "Send" button should appear in the UI. You can enter "*ping*" into the text box and hit send. This will send a JSON payload containing "*ping*" to the native messaging host. In turn, the host will respond with a JSON payload containing "*pong*". 60 | 61 | **Debugging host:** To debug the native messaging host launch Chrome with logging enabled. This will open a terminal window when Chrome is started that may contain messages related to Chrome's interaction with the native messaging host. To enable debugging and view its output, append the `--enable-logging` command to a command to launch chrome, like this: `"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --enable-logging`. You can also review the log file the native messaging host will generate. The log file will be found in the same directory as the native messaging host executable. 62 | 63 | **Note:** If you do not have a Chrome extension script maintaining a connection to the native messaging host, Chrome will close the Stdin pipe to the host. Depending on how the native messaging host is written, it may or may not close as well. In this sample app, the native host will detect that the Stdin pipe closed and it will trigger the native host to shut down. If the extension is reopened, the native host will start again. I suggest communicating with the native messaging host via a background script. That way, only 1 instance of the native host will be launched. 64 | 65 | ## License 66 | 67 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details -------------------------------------------------------------------------------- /app/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |   10 |
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | var port = null; 6 | 7 | var getKeys = function(obj){ 8 | var keys = []; 9 | for(var key in obj){ 10 | keys.push(key); 11 | } 12 | return keys; 13 | } 14 | 15 | function appendMessage(text) { 16 | document.getElementById('response').innerHTML += "

" + text + "

"; 17 | } 18 | 19 | function updateUiState() { 20 | if (port) { 21 | document.getElementById('connect-button').style.display = 'none'; 22 | document.getElementById('input-text').style.display = 'inline-block'; 23 | document.getElementById('send-message-button').style.display = 'inline-block'; 24 | } else { 25 | document.getElementById('connect-button').style.display = 'block'; 26 | document.getElementById('input-text').style.display = 'none'; 27 | document.getElementById('send-message-button').style.display = 'none'; 28 | } 29 | } 30 | 31 | function sendNativeMessage() { 32 | message = {"query": document.getElementById('input-text').value.trim()}; 33 | port.postMessage(message); 34 | appendMessage("Sent message: " + JSON.stringify(message) + ""); 35 | } 36 | 37 | function onNativeMessage(message) { 38 | appendMessage("Received message: " + JSON.stringify(message) + ""); 39 | } 40 | 41 | function onDisconnected() { 42 | appendMessage("Failed to connect: " + chrome.runtime.lastError.message); 43 | port = null; 44 | updateUiState(); 45 | } 46 | 47 | function connect() { 48 | var hostName = "com.sample.native_msg_golang"; 49 | port = chrome.runtime.connectNative(hostName); 50 | port.onMessage.addListener(onNativeMessage); 51 | port.onDisconnect.addListener(onDisconnected); 52 | appendMessage("Connected to native messaging host " + hostName + "") 53 | updateUiState(); 54 | } 55 | 56 | document.addEventListener('DOMContentLoaded', function () { 57 | document.getElementById('connect-button').addEventListener('click', connect); 58 | document.getElementById('send-message-button').addEventListener('click', sendNativeMessage); 59 | updateUiState(); 60 | }); 61 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnF7RGLAxIon0/XeNZ4MLdP3DMkoORzEAKVg0sb89JpA/W2osTHr91Wqwdc9lW0mFcSpCYS9Y3e7cUMFo/M2ETASIuZncMiUzX2/0rrWtGQ3UuEj3KSe5PdaVZfisyJw/FebvHwirEWrhqcgzVUj9fL9YjE0G45d1zMKcc1umKvLqPyTznNuKBZ9GJREdGLRJCBmUgCkI8iwtwC+QZTUppmaD50/ksnEUXv+QkgGN07/KoNA5oAgo49Jf1XBoMv4QXtVZQlBYZl84zAsI82hb63a6Gu29U/4qMWDdI7+3Ne5TRvo6Zi3EI4M2NQNplJhik105qrz+eTLJJxvf4slrWwIDAQAB", 3 | "name": "Chrome Native Messaging Go Example", 4 | "version": "1.0", 5 | "manifest_version": 2, 6 | "description": "Sends a message to Chrome native messaging application written in Go.", 7 | "app": { 8 | "launch": { 9 | "local_path": "main.html" 10 | } 11 | }, 12 | "permissions": [ 13 | "nativeMessaging" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /native-host/config/com.sample.native_msg_golang.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.sample.native_msg_golang", 3 | "description": "Chrome Native Messaging Host Go Example", 4 | "path": "ADD PATH TO nativeMsgSample.exe HERE", 5 | "type": "stdio", 6 | "allowed_origins": [ 7 | "chrome-extension://ghbmnnjooekpmoecnnnilnnbdlolhkhi/" 8 | ] 9 | } -------------------------------------------------------------------------------- /native-host/src/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: J. Farley 3 | * @Date: 2019-05-19 4 | * @Description: Basic chrome native messaging host example. 5 | */ 6 | package main 7 | 8 | import ( 9 | "bufio" 10 | "bytes" 11 | "encoding/binary" 12 | "encoding/json" 13 | "io" 14 | "log" 15 | "os" 16 | "unsafe" 17 | ) 18 | 19 | // constants for Logger 20 | var ( 21 | // Trace logs general information messages. 22 | Trace *log.Logger 23 | // Error logs error messages. 24 | Error *log.Logger 25 | ) 26 | 27 | // nativeEndian used to detect native byte order 28 | var nativeEndian binary.ByteOrder 29 | 30 | // bufferSize used to set size of IO buffer - adjust to accommodate message payloads 31 | var bufferSize = 8192 32 | 33 | // IncomingMessage represents a message sent to the native host. 34 | type IncomingMessage struct { 35 | Query string `json:"query"` 36 | } 37 | 38 | // OutgoingMessage respresents a response to an incoming message query. 39 | type OutgoingMessage struct { 40 | Query string `json:"query"` 41 | Response string `json:"response"` 42 | } 43 | 44 | // Init initializes logger and determines native byte order. 45 | func Init(traceHandle io.Writer, errorHandle io.Writer) { 46 | Trace = log.New(traceHandle, "TRACE: ", log.Ldate|log.Ltime|log.Lshortfile) 47 | Error = log.New(errorHandle, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile) 48 | 49 | // determine native byte order so that we can read message size correctly 50 | var one int16 = 1 51 | b := (*byte)(unsafe.Pointer(&one)) 52 | if *b == 0 { 53 | nativeEndian = binary.BigEndian 54 | } else { 55 | nativeEndian = binary.LittleEndian 56 | } 57 | } 58 | 59 | func main() { 60 | file, err := os.OpenFile("chrome-native-host-log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 61 | if err != nil { 62 | Init(os.Stdout, os.Stderr) 63 | Error.Printf("Unable to create and/or open log file. Will log to Stdout and Stderr. Error: %v", err) 64 | } else { 65 | Init(file, file) 66 | // ensure we close the log file when we're done 67 | defer file.Close() 68 | } 69 | 70 | Trace.Printf("Chrome native messaging host started. Native byte order: %v.", nativeEndian) 71 | read() 72 | Trace.Print("Chrome native messaging host exited.") 73 | } 74 | 75 | // read Creates a new buffered I/O reader and reads messages from Stdin. 76 | func read() { 77 | v := bufio.NewReader(os.Stdin) 78 | // adjust buffer size to accommodate your json payload size limits; default is 4096 79 | s := bufio.NewReaderSize(v, bufferSize) 80 | Trace.Printf("IO buffer reader created with buffer size of %v.", s.Size()) 81 | 82 | lengthBytes := make([]byte, 4) 83 | lengthNum := int(0) 84 | 85 | // we're going to indefinitely read the first 4 bytes in buffer, which gives us the message length. 86 | // if stdIn is closed we'll exit the loop and shut down host 87 | for b, err := s.Read(lengthBytes); b > 0 && err == nil; b, err = s.Read(lengthBytes) { 88 | // convert message length bytes to integer value 89 | lengthNum = readMessageLength(lengthBytes) 90 | Trace.Printf("Message size in bytes: %v", lengthNum) 91 | 92 | // If message length exceeds size of buffer, the message will be truncated. 93 | // This will likely cause an error when we attempt to unmarshal message to JSON. 94 | if lengthNum > bufferSize { 95 | Error.Printf("Message size of %d exceeds buffer size of %d. Message will be truncated and is unlikely to unmarshal to JSON.", lengthNum, bufferSize) 96 | } 97 | 98 | // read the content of the message from buffer 99 | content := make([]byte, lengthNum) 100 | _, err := s.Read(content) 101 | if err != nil && err != io.EOF { 102 | Error.Fatal(err) 103 | } 104 | 105 | // message has been read, now parse and process 106 | parseMessage(content) 107 | } 108 | 109 | Trace.Print("Stdin closed.") 110 | } 111 | 112 | // readMessageLength reads and returns the message length value in native byte order. 113 | func readMessageLength(msg []byte) int { 114 | var length uint32 115 | buf := bytes.NewBuffer(msg) 116 | err := binary.Read(buf, nativeEndian, &length) 117 | if err != nil { 118 | Error.Printf("Unable to read bytes representing message length: %v", err) 119 | } 120 | return int(length) 121 | } 122 | 123 | // parseMessage parses incoming message 124 | func parseMessage(msg []byte) { 125 | iMsg := decodeMessage(msg) 126 | Trace.Printf("Message received: %s", msg) 127 | 128 | // start building outgoing json message 129 | oMsg := OutgoingMessage{ 130 | Query: iMsg.Query, 131 | } 132 | 133 | switch iMsg.Query { 134 | case "ping": 135 | oMsg.Response = "pong" 136 | case "hello": 137 | oMsg.Response = "goodbye" 138 | default: 139 | oMsg.Response = "42" 140 | } 141 | 142 | send(oMsg) 143 | } 144 | 145 | // decodeMessage unmarshals incoming json request and returns query value. 146 | func decodeMessage(msg []byte) IncomingMessage { 147 | var iMsg IncomingMessage 148 | err := json.Unmarshal(msg, &iMsg) 149 | if err != nil { 150 | Error.Printf("Unable to unmarshal json to struct: %v", err) 151 | } 152 | return iMsg 153 | } 154 | 155 | // send sends an OutgoingMessage to os.Stdout. 156 | func send(msg OutgoingMessage) { 157 | byteMsg := dataToBytes(msg) 158 | writeMessageLength(byteMsg) 159 | 160 | var msgBuf bytes.Buffer 161 | _, err := msgBuf.Write(byteMsg) 162 | if err != nil { 163 | Error.Printf("Unable to write message length to message buffer: %v", err) 164 | } 165 | 166 | _, err = msgBuf.WriteTo(os.Stdout) 167 | if err != nil { 168 | Error.Printf("Unable to write message buffer to Stdout: %v", err) 169 | } 170 | } 171 | 172 | // dataToBytes marshals OutgoingMessage struct to slice of bytes 173 | func dataToBytes(msg OutgoingMessage) []byte { 174 | byteMsg, err := json.Marshal(msg) 175 | if err != nil { 176 | Error.Printf("Unable to marshal OutgoingMessage struct to slice of bytes: %v", err) 177 | } 178 | return byteMsg 179 | } 180 | 181 | // writeMessageLength determines length of message and writes it to os.Stdout. 182 | func writeMessageLength(msg []byte) { 183 | err := binary.Write(os.Stdout, nativeEndian, uint32(len(msg))) 184 | if err != nil { 185 | Error.Printf("Unable to write message length to Stdout: %v", err) 186 | } 187 | } 188 | --------------------------------------------------------------------------------