--- /dev/null
+#!/bin/bash
+
+# uipc.sh - Toolbox module for unsigned message-based IPC
+# Copyright (C) 2022 Matthias Kruk
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+__init() {
+ if ! include "json" "queue"; then
+ return 1
+ fi
+
+ declare -gxr __uipc_root="/var/lib/toolbox/uipc"
+ declare -gxr __uipc_public="$__uipc_root/pub"
+ declare -gxr __uipc_private="$__uipc_root/priv/$USER"
+ declare -gxr __uipc_group="toolbox_ipc"
+ declare -gxr __uipc_pubsub_root="$__uipc_root/pubsub"
+
+ declare -gxir __uipc_version=1
+
+ if ! mkdir -p "$__uipc_private" ||
+ ! chgrp "$__uipc_group" "$__uipc_private"; then
+ log_error "Could not initialize private UIPC directory $__uipc_private"
+ return 1
+ fi
+
+ return 0
+}
+
+_uipc_encode() {
+ local decoded="$1"
+
+ if (( $# > 0 )); then
+ base64 -w 0 <<< "$decoded"
+ else
+ base64 -w 0 < /dev/stdin
+ fi
+}
+
+_uipc_decode() {
+ local encoded="$1"
+
+ if (( $# > 0 )); then
+ base64 -d <<< "$encoded"
+ else
+ base64 -d < /dev/stdin
+ fi
+}
+
+
+_uipc_msg_get() {
+ local msg="$1"
+ local field="$2"
+
+ local value
+
+ if ! value=$(_uipc_decode "$msg" | jq -e -r ".$field" 2>/dev/null); then
+ return 1
+ fi
+
+ echo "$value"
+ return 0
+}
+
+_uipc_msg_version_supported() {
+ local msg="$1"
+
+ local -i version
+
+ if ! version=$(uipc_msg_get_version "$msg"); then
+ log_error "Could not get version from message"
+ return 1
+ fi
+
+ if (( version != __uipc_version )); then
+ log_error "Unsupported message version"
+ return 1
+ fi
+
+ return 0
+}
+
+uipc_msg_dump() {
+ local msg="$1"
+
+ local version
+ local version_ok
+
+ version_ok="no"
+ version=$(_uipc_msg_get "$msg" "version")
+
+ if _uipc_msg_version_supported "$msg"; then
+ version_ok="yes"
+ fi
+
+ cat <<EOF | log_highlight "uipc message"
+Message version: $version [supported: $version_ok]
+
+$(_uipc_decode <<< "$msg" | jq .)
+EOF
+
+ return 0
+}
+
+_uipc_msg_new() {
+ local source="$1"
+ local destination="$2"
+ local data="$3"
+
+ local encoded_data
+ local timestamp
+ local message
+ local encoded_message
+
+ if ! encoded_data=$(_uipc_encode <<< "$data"); then
+ log_error "Could not encode data"
+
+ elif ! timestamp=$(date +"%s"); then
+ log_error "Could not make timestamp"
+
+ elif ! message=$(json_object "version" "$__uipc_version" \
+ "source" "$source" \
+ "destination" "$destination" \
+ "user" "$USER" \
+ "timestamp" "$timestamp" \
+ "data" "$encoded_data"); then
+ log_error "Could not make message"
+
+ elif ! encoded_message=$(_uipc_encode "$message"); then
+ log_error "Could not encode message"
+
+ else
+ echo "$encoded_message"
+ return 0
+ fi
+
+ return 1
+}
+
+uipc_msg_get_version() {
+ local msg="$1"
+
+ local version
+
+ if ! version=$(_uipc_msg_get "$msg" "version"); then
+ return 1
+ fi
+
+ echo "$version"
+ return 0
+}
+
+uipc_msg_get_source() {
+ local msg="$1"
+
+ local src
+
+ if ! src=$(_uipc_msg_get "$msg" "source"); then
+ return 1
+ fi
+
+ echo "$src"
+ return 0
+}
+
+uipc_msg_get_destination() {
+ local msg="$1"
+
+ local dst
+
+ if ! dst=$(_uipc_msg_get "$msg" "destination"); then
+ return 1
+ fi
+
+ echo "$dst"
+ return 0
+}
+
+uipc_msg_get_user() {
+ local msg="$1"
+
+ local user
+
+ if ! user=$(_uipc_msg_get "$msg" "user"); then
+ return 1
+ fi
+
+ echo "$user"
+ return 0
+}
+
+uipc_msg_get_timestamp() {
+ local msg="$1"
+
+ local timestamp
+
+ if ! timestamp=$(_uipc_msg_get "$msg" "timestamp"); then
+ return 1
+ fi
+
+ echo "$timestamp"
+ return 0
+}
+
+uipc_msg_get_data() {
+ local msg="$1"
+
+ local data
+ local data_raw
+
+ if ! data=$(_uipc_msg_get "$msg" "data"); then
+ return 1
+ fi
+
+ if ! data_raw=$(_uipc_decode <<< "$data"); then
+ return 1
+ fi
+
+ echo "$data_raw"
+ return 0
+}
+
+uipc_endpoint_open() {
+ local name="$1"
+
+ local endpoint
+
+ if [[ -z "$name" ]]; then
+ local self
+
+ self="${0##*/}"
+ name="priv/$USER/$self.$$.$(date +"%s").$RANDOM"
+ fi
+
+ endpoint="$__uipc_root/$name"
+
+ if ! [ -d "$endpoint" ]; then
+ if ! mkdir -p "$endpoint/subscriptions"; then
+ return 1
+ fi
+
+ if ! queue_init "$endpoint/queue" ||
+ ! echo "$USER" > "$endpoint/owner"; then
+ if ! rm -rf "$endpoint"; then
+ log_error "Could not clean up $endpoint"
+ fi
+
+ return 1
+ fi
+ fi
+
+ echo "$name"
+ return 0
+}
+
+uipc_endpoint_close() {
+ local name="$1"
+
+ local endpoint
+ local subscription
+
+ endpoint="$__uipc_root/$name"
+
+ if ! queue_destroy "$endpoint/queue"; then
+ return 1
+ fi
+
+ while read -r subscription; do
+ if ! rm "$subscription/${name//\//_}"; then
+ log_error "Could not unsubscribe $name from $subscription"
+ fi
+ done < <(find "$endpoint/subscriptions" -mindepth 1 -maxdepth 1 -type l)
+
+ if ! rm -rf "$endpoint"; then
+ return 1
+ fi
+
+ return 0
+}
+
+_uipc_endpoint_put() {
+ local endpoint="$1"
+ local msg="$2"
+
+ local queue
+
+ queue="$__uipc_root/$endpoint/queue"
+
+ if ! queue_put "$queue" "$msg"; then
+ return 1
+ fi
+
+ return 0
+}
+
+_uipc_endpoint_get() {
+ local endpoint="$1"
+ local -i timeout="$2"
+
+ local queue
+ local msg
+
+ queue="$__uipc_root/$endpoint/queue"
+
+ if ! msg=$(queue_get "$queue" "$timeout"); then
+ return 1
+ fi
+
+ echo "$msg"
+ return 0
+}
+
+uipc_endpoint_send() {
+ local source="$1"
+ local destination="$2"
+ local data="$3"
+
+ local msg
+
+ if ! msg=$(_uipc_msg_new "$source" "$destination" "$data"); then
+ return 1
+ fi
+
+ if ! _uipc_endpoint_put "$destination" "$msg"; then
+ return 1
+ fi
+
+ return 0
+}
+
+uipc_endpoint_recv() {
+ local endpoint="$1"
+ local -i timeout="$2"
+
+ local -i start
+
+ if (( $# < 2 )); then
+ timeout=-1
+ fi
+
+ if ! start=$(date +"%s"); then
+ return 2
+ fi
+
+ while true; do
+ local msg
+ local -i elapsed
+ local -i remaining
+
+ remaining="$timeout"
+
+ if (( timeout > 0 )); then
+ local now
+
+ if ! now=$(date +"%s"); then
+ return 2
+ fi
+
+ elapsed=$((now - start))
+ remaining=$((timeout - elapsed))
+
+ # Remaining must not be negative because _uipc_endpoint_get() takes
+ # that to mean "block (possibly forever) until a message arrives"
+ if (( remaining < 0 )); then
+ remaining=0
+ fi
+ fi
+
+ if msg=$(_uipc_endpoint_get "$endpoint" "$remaining"); then
+ echo "$msg"
+ return 0
+ fi
+
+ if (( remaining == 0 )); then
+ break
+ fi
+ done
+
+ return 1
+}
+
+_uipc_endpoint_topic_create() {
+ local topic="$1"
+
+ if ! mkdir -p "$__uipc_pubsub_root/$topic"; then
+ return 1
+ fi
+
+ return 0
+}
+
+_uipc_endpoint_topic_subscribe() {
+ local endpoint="$1"
+ local topic="$2"
+
+ local topicdir
+ local subscription
+
+ topicdir="$__uipc_pubsub_root/$topic"
+ subscription="$topicdir/${endpoint//\//_}"
+
+ if ! ln -sf "$endpoint" "$subscription"; then
+ return 1
+ fi
+
+ if ! ln -sfn "$topicdir" "$__uipc_root/$endpoint/subscriptions/$topic"; then
+ rm -f "$subscription"
+ return 1
+ fi
+
+ return 0
+}
+
+_uipc_endpoint_topic_get_subscribers() {
+ local topic="$1"
+
+ local subscription
+
+ while read -r subscription; do
+ local subscriber
+
+ if ! subscriber=$(readlink "$subscription"); then
+ continue
+ fi
+
+ echo "$subscriber"
+ done < <(find "$__uipc_pubsub_root/$topic" -mindepth 1 -maxdepth 1 -type l)
+
+ return 0
+}
+
+uipc_endpoint_subscribe() {
+ local endpoint="$1"
+ local topic="$2"
+
+ if ! _uipc_endpoint_topic_create "$topic"; then
+ return 1
+ fi
+
+ if ! _uipc_endpoint_topic_subscribe "$endpoint" "$topic"; then
+ return 1
+ fi
+
+ return 0
+}
+
+uipc_endpoint_publish() {
+ local endpoint="$1"
+ local topic="$2"
+ local message="$3"
+
+ local subscriber
+
+ if ! _uipc_endpoint_topic_create "$topic"; then
+ return 1
+ fi
+
+ while read -r subscriber; do
+ uipc_endpoint_send "$endpoint" "$subscriber" "$message"
+ done < <(_uipc_endpoint_topic_get_subscribers "$topic")
+
+ return 0
+}
+
+_uipc_endpoint_foreach_message_helper() {
+ local msg="$1"
+ local endpoint="$2"
+ local func="$3"
+ local args=("${@:4}")
+
+ "$func" "$endpoint" "$msg" "${args[@]}"
+ return "$?"
+}
+
+uipc_endpoint_foreach_message() {
+ local endpoint="$1"
+ local func="$2"
+ local args=("${@:3}")
+
+ local queue
+
+ queue="$__uipc_root/$endpoint/queue"
+
+ if ! queue_foreach "$queue" _uipc_endpoint_foreach_message_helper \
+ "$endpoint" "$func" "${args[@]}"; then
+ return 1
+ fi
+
+ return 0
+}
--- /dev/null
+#!/bin/bash
+
+# uipc_spec.sh - Test cases for the toolbox uipc module
+# Copyright (C) 2022 Matthias Kruk
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+. toolbox.sh
+include "uipc"
+
+setup() {
+ return 0
+}
+
+cleanup() {
+ return 0
+}
+
+Describe "Encoding"
+ It "_uipc_encode() outputs base64"
+ _test_encoding() {
+ local data
+
+ data=$(dd if=/dev/urandom bs=1024 count=1024 2>/dev/null |
+ _uipc_encode)
+
+ if ! is_base64 "$data"; then
+ return 1
+ fi
+
+ return 0
+ }
+
+ When call _test_encoding
+ The status should equal 0
+ End
+
+ It "_uipc_encode() output has correct length"
+ _test_encoding_length() {
+ local data
+ local block_size
+ local block_num
+ local input_bytes
+ local input_bits
+ local expected_length
+ local actual_length
+
+ block_size=1024
+ block_num=1024
+ input_bytes=$((block_size * block_num))
+ input_bits=$((input_bytes * 8))
+
+ actual_length=$(dd if=/dev/urandom bs="$block_size" count="$block_num" 2>/dev/null |
+ _uipc_encode | wc -c)
+
+ if (( input_bits % 24 > 0 )); then
+ # data is padded
+ (( input_bits += 24 - (input_bits % 24) ))
+ fi
+ expected_length=$((input_bits / 6))
+
+ if (( expected_length != actual_length )); then
+ return 1
+ fi
+
+ return 0
+ }
+
+ When call _test_encoding_length
+ The status should equal 0
+ End
+
+ It "_uipc_encode() output does not contain newlines"
+ _test_encoding_newlines() {
+ local lines
+
+ lines=$(dd if=/dev/urandom bs=1024 count=1024 2>/dev/null |
+ _uipc_encode | wc -l)
+
+ if (( lines != 0 )); then
+ return 1
+ fi
+
+ return 0
+ }
+
+ When call _test_encoding_newlines
+ The status should equal 0
+ End
+
+ It "_uipc_decode() reverses _ipc_encode()"
+ _test_encode_decode() {
+ local data_before
+ local data_encoded
+ local data_after
+
+ data_before=$(dd if=/dev/urandom bs=1024 count=1024 2>/dev/null | base64 -w 0)
+ data_encoded=$(_uipc_encode <<< "$data_before")
+ data_after=$(_uipc_decode <<< "$data_encoded")
+
+ if [[ "$data_before" != "$data_after" ]]; then
+ return 1
+ fi
+
+ return 0
+ }
+
+ When call _test_encode_decode
+ The status should equal 0
+ End
+End
+
+Describe "Message"
+ BeforeAll 'setup'
+ AfterAll 'cleanup'
+
+ It "_uipc_msg_new() outputs base64 encoded data"
+ _test_uipc_msg_new_is_base64() {
+ local msg
+
+ if ! msg=$(_uipc_msg_new "from" "to" "data"); then
+ return 1
+ fi
+
+ if ! is_base64 "$msg"; then
+ return 1
+ fi
+
+ return 0
+ }
+
+ When call _test_uipc_msg_new_is_base64
+ The status should equal 0
+ End
+
+ It "_uipc_msg_new() outputs an encoded JSON object"
+ _test_uipc_msg_new_is_json() {
+ local msg
+
+ if ! msg=$(_uipc_msg_new "from" "to" "data"); then
+ return 1
+ fi
+
+ if ! _uipc_decode <<< "$msg" | jq -r -e . ; then
+ return 1
+ fi
+
+ return 0
+ }
+
+ When call _test_uipc_msg_new_is_json
+ The status should equal 0
+ The stdout should match pattern '{*}'
+ The stderr should not start with "parse error"
+ End
+
+ It "_uipc_msg_new() generates valid toolbox.ipc.message objects"
+ _test_uipc_msg_new_json_schema_envelope() {
+ local msg
+
+ if ! msg=$(_uipc_msg_new "from" "to" "data"); then
+ return 1
+ fi
+
+ if ! ../spec/validate.py ../spec/ipc_message.schema.json <(_uipc_decode "$msg"); then
+ return 1
+ fi
+
+ return 0
+ }
+
+ When call _test_uipc_msg_new_json_schema_envelope
+ The status should equal 0
+ End
+
+ It "_uipc_msg_new()/uipc_msg_get_version() sets/gets the correct version"
+ _test_uipc_msg_new_version() {
+ local msg
+
+ if ! msg=$(_uipc_msg_new "from" "to" "data"); then
+ return 1
+ fi
+
+ uipc_msg_get_version "$msg"
+ }
+
+ When call _test_uipc_msg_new_version
+ The status should equal 0
+ The stdout should equal "$__uipc_version"
+ End
+
+ It "_uipc_msg_new()/uipc_msg_get_user() sets/gets the correct user"
+
+ _test_uipc_msg_new_user() {
+ local msg
+
+ msg=$(_uipc_msg_new "from" "to" "data")
+
+ uipc_msg_get_user "$msg"
+ }
+
+ When call _test_uipc_msg_new_user
+ The status should equal 0
+ The stdout should equal "$USER"
+ End
+
+ It "_uipc_msg_new()/uipc_msg_get_timestamp() sets/gets the correct timestamp"
+ _test_uipc_msg_new_timestamp() {
+ local before
+ local after
+ local msg
+ local timestamp
+
+ before=$(date +"%s")
+ msg=$(_uipc_msg_new "from" "to" "data")
+ after=$(date +"%s")
+
+ timestamp=$(uipc_msg_get_timestamp "$msg")
+
+ if (( timestamp < before )) ||
+ (( timestamp > after )); then
+ return 1
+ fi
+
+ return 0
+ }
+
+ When call _test_uipc_msg_new_timestamp
+ The status should equal 0
+ End
+
+ It "_uipc_msg_new()/uipc_msg_get_source() sets/gets the correct source"
+ _test_uipc_msg_new_source() {
+ local msg
+
+ if ! msg=$(_uipc_msg_new "from" "to" "data"); then
+ return 1
+ fi
+
+ uipc_msg_get_source "$msg"
+ }
+
+ When call _test_uipc_msg_new_source
+ The status should equal 0
+ The stdout should equal "from"
+ End
+
+ It "_uipc_msg_new()/uipc_msg_get_destination() sets/gets the correct destination"
+ _test_uipc_msg_new_destination() {
+ local msg
+
+ if ! msg=$(_uipc_msg_new "from" "to" "data"); then
+ return 1
+ fi
+
+ uipc_msg_get_destination "$msg"
+ }
+
+ When call _test_uipc_msg_new_destination
+ The status should equal 0
+ The stdout should equal "to"
+ End
+
+ It "_uipc_msg_new()/uipc_msg_get_data() sets/gets the correct data"
+ _test_uipc_msg_new_data() {
+ local msg
+
+ if ! msg=$(_uipc_msg_new "from" "to" "data"); then
+ return 1
+ fi
+
+ uipc_msg_get_data "$msg"
+ }
+
+ When call _test_uipc_msg_new_data
+ The status should equal 0
+ The stdout should equal "data"
+ End
+End
+
+Describe "uipc_endpoint_open"
+ It "opens a public endpoint when the endpoint name is specified"
+ _test_uipc_endpoint_open_public() {
+ local endpoint_name
+ local endpoint
+ local res
+
+ endpoint_name="pub/test$RANDOM"
+ res=1
+
+ if endpoint=$(uipc_endpoint_open "$endpoint_name"); then
+ if [[ "$endpoint" != "priv/"* ]]; then
+ res=0
+ fi
+
+ uipc_endpoint_close "$endpoint"
+ fi
+
+ return "$res"
+ }
+
+ When call _test_uipc_endpoint_open_public
+ The status should equal 0
+ End
+
+ It "opens a private endpoint when no endpoint name is specified"
+ _test_uipc_endpoint_open_private() {
+ local endpoint
+ local res
+
+ res=1
+
+ if endpoint=$(uipc_endpoint_open); then
+ if [[ "$endpoint" == "priv/"* ]]; then
+ res=0
+ fi
+
+ uipc_endpoint_close "$endpoint"
+ fi
+
+ return "$res"
+ }
+
+ When call _test_uipc_endpoint_open_private
+ The status should equal 0
+ End
+End
+
+Describe "uipc_endpoint_close"
+ It "closes a public endpoint"
+ _test_uipc_endpoint_close_public() {
+ local endpoint
+
+ if ! endpoint=$(uipc_endpoint_open "pub/test$RANDOM"); then
+ return 1
+ fi
+
+ if ! uipc_endpoint_close "$endpoint"; then
+ return 1
+ fi
+
+ return 0
+ }
+
+ When call _test_uipc_endpoint_close_public
+ The status should equal 0
+ End
+
+ It "closes a private endpoint"
+ _test_uipc_endpoint_close_private() {
+ local endpoint
+
+ if ! endpoint=$(uipc_endpoint_open); then
+ return 1
+ fi
+
+ if ! uipc_endpoint_close "$endpoint"; then
+ return 1
+ fi
+
+ return 0
+ }
+
+ When call _test_uipc_endpoint_close_private
+ The status should equal 0
+ End
+End
+
+Describe "uipc_endpoint_send"
+ BeforeAll 'setup'
+ AfterAll 'cleanup'
+
+ It "sends a message to a public endpoint"
+ _test_uipc_endpoint_send_public() {
+ local endpoint
+ local res
+
+ if ! endpoint=$(uipc_endpoint_open "pub/test$RANDOM"); then
+ return 1
+ fi
+
+ if uipc_endpoint_send "-" "$endpoint" "data"; then
+ res=0
+ else
+ res=1
+ fi
+
+ uipc_endpoint_close "$endpoint"
+
+ return "$res"
+ }
+
+ When call _test_uipc_endpoint_send_public
+ The status should equal 0
+ End
+
+ It "sends a message to a private endpoint"
+ _test_uipc_endpoint_send_private() {
+ local endpoint
+ local res
+
+ if ! endpoint=$(uipc_endpoint_open); then
+ return 1
+ fi
+
+ if uipc_endpoint_send "-" "$endpoint" "data"; then
+ res=0
+ else
+ res=1
+ fi
+
+ uipc_endpoint_close "$endpoint"
+
+ return "$res"
+ }
+
+ When call _test_uipc_endpoint_send_private
+ The status should equal 0
+ End
+End
+
+Describe "uipc_endpoint_recv"
+ BeforeAll 'setup'
+ AfterAll 'cleanup'
+
+ It "receives messages on a public endpoint"
+ _test_uipc_endpoint_recv_public() {
+ local endpoint
+ local res
+ local txdata
+ local rxdata
+ local msg
+
+ txdata="data$RANDOM"
+ res=1
+
+ if endpoint=$(uipc_endpoint_open "pub/test$RANDOM"); then
+ if ! uipc_endpoint_send "-" "$endpoint" "$txdata"; then
+ res=2
+
+ elif ! msg=$(uipc_endpoint_recv "$endpoint" 10); then
+ res=3
+
+ elif ! rxdata=$(uipc_msg_get_data "$msg"); then
+ res=4
+
+ elif [[ "$rxdata" != "$txdata" ]]; then
+ res=5
+
+ else
+ res=0
+ fi
+
+ uipc_endpoint_close "$endpoint"
+ fi
+
+ return "$res"
+ }
+
+ When call _test_uipc_endpoint_recv_public
+ The status should equal 0
+ End
+
+ It "receives messages on a private endpoint"
+ _test_uipc_endpoint_recv_private() {
+ local endpoint
+ local res
+ local txdata
+ local rxdata
+ local msg
+
+ res=1
+ txdata="data$RANDOM"
+
+ if endpoint=$(uipc_endpoint_open); then
+ if ! uipc_endpoint_send "-" "$endpoint" "$txdata"; then
+ res=2
+
+ elif ! msg=$(uipc_endpoint_recv "$endpoint"); then
+ res=3
+
+ elif ! rxdata=$(uipc_msg_get_data "$msg"); then
+ res=4
+
+ elif [[ "$rxdata" != "$txdata" ]]; then
+ res=5
+
+ else
+ res=0
+ fi
+
+ uipc_endpoint_close "$endpoint"
+ fi
+
+ return "$res"
+ }
+
+ When call _test_uipc_endpoint_recv_private
+ The status should equal 0
+ End
+End