]> git.corax.cc Git - toolbox/commitdiff
include/uipc: Add module for unsigned message-based IPC
authorMatthias Kruk <m@m10k.eu>
Sun, 27 Feb 2022 08:35:27 +0000 (17:35 +0900)
committerMatthias Kruk <m@m10k.eu>
Thu, 10 Mar 2022 06:29:25 +0000 (15:29 +0900)
The IPC module cannot be used without GPG for message signing and
verification, which makes the module unusable if GPG cannot be used.

This commit adds the uipc module, which implements functions similar
to those of the ipc module, but without message authentication.

include/uipc.sh [new file with mode: 0644]
test/uipc_spec.sh [new file with mode: 0644]

diff --git a/include/uipc.sh b/include/uipc.sh
new file mode 100644 (file)
index 0000000..e5e597a
--- /dev/null
@@ -0,0 +1,501 @@
+#!/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
+}
diff --git a/test/uipc_spec.sh b/test/uipc_spec.sh
new file mode 100644 (file)
index 0000000..0a21795
--- /dev/null
@@ -0,0 +1,512 @@
+#!/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