How do I use a shell-script as Chrome Native Messaging host application
Asked Answered
B

3

6

How do you process a Chrome Native Messaging API-call with a bash script?

I succeeded in doing it with python with this example

Sure I can call bash from the python code with subprocess, but is it possible to skip python and process the message in bash directly?

The problematic part is reading the JSON serialized message into a variable. The message is serialized using JSON, UTF-8 encoded and is preceded with 32-bit message length in native byte order through stdin.

echo $* only outputs: chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/

Also something like

read
echo $REPLY

doesn't output anything. No sign of the JSON message. Python uses struct.unpack for this. Can that be done in bash?

Buster answered 15/7, 2014 at 17:38 Comment(1)
@heinst "write a shell script and use curl" - and how would that exactly solve this problem...?Congratulant
C
8

I suggest to not use (bash) shell scripts as a native messaging host, because bash is too limited to be useful.

read without any parameters reads a whole line before terminating, while the native messaging protocol specifies that the first four bytes specify the length of the following message (in native byte order).

Bash is a terrible tool for processing binary data. An improved version of your read command would specify the -n N parameter to stop reading after N characters (note: not bytes) and -r to remove some processing. E.g. the following would store the first four characters in a variable called var_prefix:

IFS= read -rn 4 var_prefix

Even if you assume that this stores the first four bytes in the variable (it does not!), then you have to convert the bytes to an integer. Did I already mention that bash automatically drops all NUL bytes? This characteristics makes Bash utterly worthless for being a fully capable native messaging host.

You could cope with this shortcoming by ignoring the first few bytes, and start parsing the result when you spot a { character, the beginning of the JSON-formatted request. After this, you have to read all input until the end of the input is found. You need a JSON parser that stops reading input when it encounters the end of the JSON string. Good luck with writing that.

Generating output is a easier, just use echo -n or printf.

Here is a minimal example that assumes that the input ends with a }, reads it (without processing) and replies with a result. Although this demo works, I strongly recommend to not use bash, but a richer (scripting) language such as Python or C++.

#!/bin/bash
# Loop forever, to deal with chrome.runtime.connectNative
while IFS= read -r -n1 c; do
    # Read the first message
    # Assuming that the message ALWAYS ends with a },
    # with no }s in the string. Adopt this piece of code if needed.
    if [ "$c" != '}' ] ; then
        continue
    fi

    message='{"message": "Hello world!"}'
    # Calculate the byte size of the string.
    # NOTE: This assumes that byte length is identical to the string length!
    # Do not use multibyte (unicode) characters, escape them instead, e.g.
    # message='"Some unicode character:\u1234"'
    messagelen=${#message}

    # Convert to an integer in native byte order.
    # If you see an error message in Chrome's stdout with
    # "Native Messaging host tried sending a message that is ... bytes long.",
    # then just swap the order, i.e. messagelen1 <-> messagelen4 and
    # messagelen2 <-> messagelen3
    messagelen1=$(( ($messagelen      ) & 0xFF ))               
    messagelen2=$(( ($messagelen >>  8) & 0xFF ))               
    messagelen3=$(( ($messagelen >> 16) & 0xFF ))               
    messagelen4=$(( ($messagelen >> 24) & 0xFF ))               

    # Print the message byte length followed by the actual message.
    printf "$(printf '\\x%x\\x%x\\x%x\\x%x' \
        $messagelen1 $messagelen2 $messagelen3 $messagelen4)%s" "$message"

done
Congratulant answered 16/7, 2014 at 9:28 Comment(1)
Great answer. I was afraid of this. On the other hand we shouldn't try to hit a nail with a saw. Python is fine, and once again proven: batteries includedBuster
T
1

The message reading and writing can be done in pure bash, and without using any subshells.

The tricky part is reading the first 4 bytes as a number. This can be done by setting LC_ALL=C, which will make bash treat each byte as a separate character, and using the following options for read:

  • -r to prevent backslash processing
  • -d '' to have read return 0 after reading a null byte
  • -n 1 to read one character (=byte) at a time

We also use the default output variable REPLY, which prevents whitespace stripping from the input.

To convert a character C to its numeric value, we use printf %d "'C".

After we calculate the length by applying the appropriate bit shifts to the values above, we can use read -r -N LEN to read exactly that many bytes from the input.

LC_ALL=C
readmsg() {
    # return non-zero if fewer than 4 bytes of input available
    # or if message is shorter than length specified by first 
    # 4 bytes of input
    # otherwise, set the variable $msg and return 0

    local -i i n len=0
    local REPLY
    for ((i=0; i<4; i++)); do
        read -r -d '' -n 1 || return
        printf -v n %d "'$REPLY"
        len+='n<<i*8'
    done
    read -r -N "$len" && ((${#REPLY}==len)) && msg=$REPLY
}

sendmsg() {
    local x
    # the message length as 4 hex bytes
    printf -v x %08X "${#1}"
    # write each of the 4 bytes
    printf %b "\x${x:6:2}\x${x:4:2}\x${x:2:2}\x${x:0:2}"
    # write the message itself
    printf %s "$1"
}

while readmsg; do
    # do_something "$msg"
    response='{"echo": "foo"}'
    sendmsg "$response"
done
Triviality answered 25/6, 2023 at 6:2 Comment(3)
spellcheck warns for printf -v n %d "'$REPLY" fixed by len+=$((n<<i*8)). Working example modified to echo input github.com/guest271314/NativeMessagingHosts/blob/main/….Havildar
I'm assuming you mean shellcheck and that the warning is for the line after the printf -v one? If so, changing len='...' to len=$((...)) does not matter here because the len variable was created with local -i so all assignments already undergo arithmetic expansion.Triviality
You can run spellcheck on your original code yourself to observe the warning. I linked to where I got the code to get rid of the warning, and where I adjusted your code to echo back input to the client.Havildar
H
0

This is how I wound up doing this in Bash, with GNU Coreutils head and tail (which do create subprocesses), to echo input back to client from host.

#!/bin/bash
# Bash Native Messaging host
set -x
set -o posix
getMessage() {
  # https://lists.gnu.org/archive/html/help-bash/2023-06/msg00036.html
  # length=$(busybox dd iflag=fullblock bs=4 count=1 | busybox od -An -td4)
  # message=$(busybox dd iflag=fullblock bs=$((length)) count=1)
  length=$(head -q -z --bytes=4 -| od -An -td4 -)
  message=$(head -q -z --bytes=$((length)) -)
  sendMessage "$message"
}
# https://stackoverflow.com/a/24777120
sendMessage() {
  message="$*"
  # Calculate the byte size of the string.
  # NOTE: This assumes that byte length is identical to the string length!
  # Do not use multibyte (unicode) characters, escape them instead, e.g.
  # message='"Some unicode character:\u1234"'
  messagelen=${#message}
  # Convert to an integer in native byte order.
  # If you see an error message in Chrome's stdout with
  # "Native Messaging host tried sending a message that is ... bytes long.",
  # then just swap the order, i.e. messagelen1 <-> messagelen4 and
  # messagelen2 <-> messagelen3
  messagelen1=$(((messagelen) & 0xFF))
  messagelen2=$(((messagelen >> 8) & 0xFF))
  messagelen3=$(((messagelen >> 16) & 0xFF))
  messagelen4=$(((messagelen >> 24) & 0xFF))
  # Print the message byte length followed by the actual message.
  printf "$(printf '\\x%x\\x%x\\x%x\\x%x' \
    $messagelen1 $messagelen2 $messagelen3 $messagelen4)%s" "$message"
}

main() {
  while true; do
    getMessage
  done
}

main

Havildar answered 1/3 at 4:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.