2022-02-20 09:43:11 -05:00
#!/usr/bin/env bash
VERSION = "1.0.0"
CONFIG_DIR = " $HOTPOCKET_CONFIG_DIR "
set -u
set +H
# This will be removed, dont rely on it
[ -z " $CONFIG_DIR " ] && echo "main: HOTPOCKET_CONFIG_DIR is not defined, assuming working directory. This is potentially unsafe"
[ -z " $CONFIG_DIR " ] && CONFIG_DIR = " $PWD "
SCHEMA_DIR = " $CONFIG_DIR /scripts/schema "
CONFIGD_DIR = " $CONFIG_DIR /config.d "
IGNORED_FILE = " $CONFIG_DIR /ignored.txt "
echo " main: Running as $( whoami) . Version $VERSION . Directory at $CONFIG_DIR "
die ( ) { echo " $@ " 1>& 2; exit 1; }
stripCtrlChars ( ) { sed 's/\x1b/\\x1b/g' /dev/stdin; return $? ; }
[ -d " $CONFIG_DIR " ] || die "main: No hotpocket directory defined! See readme."
[ -d " $SCHEMA_DIR " ] || die "main: No schema directory defined! See readme."
[ -d " $CONFIGD_DIR " ] || die "main: No config.d directory defined! See readme."
[ -f " $IGNORED_FILE " ] || die " main: No ignored.txt exists! You'll need to create one that $( whoami) can write to. "
str_startswith ( ) {
# str_startsswith: prefix string
local alen
local blen
local end
alen = ${# 1 }
blen = ${# 2 }
if [ " $alen " -gt " $blen " ] ; then
return 1
else
if [ " $1 " = = " ${ 2 : 0 : $alen } " ] ; then
return 0
else
return 1
fi
fi
}
str_endswith ( ) {
# str_endswith: affix string
local alen
local blen
local end
alen = ${# 1 }
blen = ${# 2 }
if [ " $alen " -gt " $blen " ] ; then
return 1
else
end = $(( blen - alen))
if [ " $1 " = = " ${ 2 : $end : $alen } " ] ; then
return 0
else
return 1
fi
fi
}
# Merge all files in config.d/
createConfig ( ) {
# createConfig:
local filepath
local filebase
local json
local code
local config
config = "{}"
for filepath in " $CONFIGD_DIR / " *; do
filebase = " $( basename " $filepath " ) "
if [ -d " $filepath " ] ; then
echo " createConfig: Ignoring $filebase , Is a directory. " 1>& 2
elif str_startswith "~" " $filebase " || str_endswith "~" " $filebase " ; then
echo " createConfig: Ignoring $filebase . Is a temporary file! " 1>& 2
elif str_startswith "." " $filebase " ; then
echo " createConfig: Ignoring $filebase . Is a hidden file! " 1>& 2
elif str_endswith ".yml" " $filebase " || str_endswith ".yaml" " $filebase " || str_endswith ".json" " $filebase " ; then
json = " $( yq . " $filepath " -c) "
code = " $? "
if [ $code -eq 0 ] ; then
config = " $( jq --argjson a " $config " --argjson b " $json " \
' $a * $b * {
policy_rooms: ( $a .policy_rooms + $b .policy_rooms) ,
actions: ( $a .actions + $b .actions)
} ' -nc) " || die " createConfig: Failed to merge $filebase with in-memory config. Exited with code $? "
else
echo " createConfig: Failed to load $filebase . Exited with code $code " 1>& 2
exit $code
fi
else
echo " createConfig: Ignoring $( basename " $filebase " ) . " 1>& 2
fi
done
#jq . 1>&2 <<< "$config"
jq . <<< " $config "
}
CONFIG = " $( createConfig) " || die " main: Failed to load config.d! Exited with $? "
yajsv -q -s " $SCHEMA_DIR /config.yaml " /dev/stdin <<< " $CONFIG " || die "main: Failed to load config.yaml! Defined config does not validate against schema"
BASE_URL = " $( jq '.base_url' -r <<< " $CONFIG " ) " || die "main: Failed to load config.yaml! Failed to read base_url"
SYNAPSE_BASE_URL = " $( jq '.synapse_base_url' -r <<< " $CONFIG " ) " || die "main: Failed to load config.yaml! Failed to read synapse_base_url"
KEYRING_FILE = " $( jq '.keyring' -r <<< " $CONFIG " ) " || die "main: Failed to load config.yaml! Failed to read keyring"
KEYRING_FILE = " $( realpath " $( realpath --relative-to= " $CONFIG_DIR " " $KEYRING_FILE " ) " ) "
[ -f " $KEYRING_FILE " ] || die " main: Defined keyring does not exist! Should exist at $KEYRING_FILE "
HAS_AUDIT_ROOM = "" ; jq -e '.audit_room | type == "string"' 1>/dev/null <<< " $CONFIG " && HAS_AUDIT_ROOM = "1"
DRY_RUN = "" ; jq -e '.dry_run' 1>/dev/null <<< " $CONFIG " && DRY_RUN = "1"
ACTIONS = " $( jq .actions -c <<< " $CONFIG " ) " || die "main: Failed to load config.yaml! Failed to read actions"
ACCESS_TOKEN = " $( jq '.access_token' -r <<< " $CONFIG " ) " || die "main: Failed to read access_token!"
USER_ID = " $( jq '.user_id' -r <<< " $CONFIG " ) " || die "main: Failed to read user_id!"
TOPICS = " $( yq -c . " $SCHEMA_DIR /topics.yaml " ) " || die "main: Failed to load topics!"
echo "main: This is a dry run, any actions will fail!" 1>& 2
readonly CONFIG
readonly ACTIONS
readonly ACCESS_TOKEN
readonly TOPICS
readonly USER_ID
readonly DRY_RUN
fetchRoomState ( ) {
# fetchRoomState: room_id
local room_id
room_id = " $( jq '@uri' -Rr <<< " $1 " ) " || return 1
curl -sf -H " Authorization: Bearer $ACCESS_TOKEN " " $BASE_URL /client/r0/rooms/ $room_id /state "
return $?
}
fetchRoomPolicies ( ) {
# fetchRoomPolicies: room_id
local states
states = " $( fetchRoomState " $1 " ) " || return 1
jq '[ .[] | select(.type == "club.glowers.policy.rule.collection") ]' -c <<< " $states "
return $?
}
arrayIndexList ( ) {
# eachInArray: array
local length
length = " $( jq 'length' -r <<< " $1 " ) "
if [ -n " $length " ] ; then
seq 1 " $length "
fi
}
# satan
# TODO: Rewrite this fucking abomination as a jq filter
arrayAndIncludes ( ) {
# arrayAndIncludes: Array<String> Array<String>
local bset
local aset_index ; local a
local bset_index ; local b
bset = " $( arrayIndexList " $2 " ) "
for aset_index in $( arrayIndexList " $1 " ) ; do
a = " $( jq " .[ $(( aset_index-1)) ] " -rc <<< " $1 " ) "
for bset_index in $bset ; do
b = " $( jq " .[ $(( bset_index-1)) ] " -rc <<< " $2 " ) "
if [ " $a " = = " $b " ] ; then
echo " $a "
return 0
fi
done
done
return 1
}
# shellcheck disable=SC2155
genIgnorefileLine ( ) {
# genIgnorefileLine: state
local room_id = " $( jq .room_id -r <<< " $1 " ) "
local state_key = " $( jq .state_key -r <<< " $1 " ) "
echo " $room_id $state_key "
}
checkPolicyIgnored ( ) {
# checkPolicyIgnored: state
grep -Fx " $( genIgnorefileLine " $1 " ) " " $IGNORED_FILE " 1>/dev/null # || return 0
}
checkSignature ( ) {
# checkSignature: state
local content
local signature
local mask
local wd
local code
content = " $( jq '.content | { rules: .rules, signature: null }' -c <<< " $1 " ) " || return 1
signature = " $( jq .content.signature -r <<< " $1 " ) " || return 2
mask = " $( umask ) "
umask 0077
wd = " $( mktemp -d) " || return 3
echo " $signature " > " $wd /content.json.sig "
echo " $content " > " $wd /content.json "
umask " $mask "
gpg --no-default-keyring --keyring " $KEYRING_FILE " --verify " $wd /content.json.sig "
code = " $? "
if [ " $code " != "0" ] ; then
2022-02-20 10:09:18 -05:00
echo " checkSignature: Exception when verifying policy signature. Exit code: $code " 1>& 2
2022-02-20 09:43:11 -05:00
rm " $wd /content.json.sig " " $wd /content.json "
rmdir " $wd "
return 4
fi
rm " $wd /content.json.sig " " $wd /content.json "
rmdir " $wd "
return 0
}
checkPolicyAgainstSchema ( ) {
# checkPolicyAgainstSchema: policy
yajsv -q -s " $SCHEMA_DIR /policy.yaml " /dev/stdin <<< " $( jq .content -c <<< " $1 " ) "
}
oneOfTagsAsArray ( ) {
# oneOfTagsAsArray: policy
jq 'if (.tag | type == "string") then [.tag] else if (.tags | type == "array") then .tags else [] end end' -c < /dev/stdin
}
applyTopicsToTags ( ) {
# applyTopicsToTags:
jq --argjson aliases " $TOPICS " '[foreach ($aliases | to_entries[]) as $alias (.; if any($alias.value[] == .) then . + [$alias.key] else . end)] | last' --< /dev/stdin
return $?
}
deployRoomShutdown ( ) {
# deployRoomShutdown: room_id action
[ -n " $DRY_RUN " ] && return 0
local room_id
local status
local payload
local reason
room_id = " $( jq '@uri' -Rr <<< " $1 " ) " || return 1
# FIXME: No proper error handling if an error thrown here.
status = " $( curl -sf -H " Authorization: Bearer $ACCESS_TOKEN " " $SYNAPSE_BASE_URL /admin/v1/rooms/ $room_id /block " ) " || return 1
#status='{"block": false}'
if ! jq -e '.block' 1>/dev/null <<< " $status " ; then
payload = '{"block": true}'
# TODO: Add flag for .purge?
if ! jq -e '.room.silent' 1>/dev/null <<< " $2 " ; then
payload = " $( jq --argjson pl " $payload " --arg user_id " $USER_ID " '$pl * { new_room_user_id: $user_id }' -nc) "
fi
reason = " $( jq '.room.reason' -r <<< " $2 " ) "
if jq -e '.room.reason' 1>/dev/null <<< " $2 " ; then
payload = " $( jq --argjson pl " $payload " --arg reason " $reason " '$pl * { reason: $reason }' -nc) "
fi
#echo "$payload" 1>&2
curl -sf -X DELETE -d " $payload " \
-H 'Content-Type: application/json' \
-H " Authorization: Bearer $ACCESS_TOKEN " " $SYNAPSE_BASE_URL /admin/v1/rooms/ $room_id /block "
return $?
fi
}
deployRoomDelist ( ) {
# deployRoomDelist: room_id action
[ -n " $DRY_RUN " ] && return 0
local room_id
room_id = " $( jq '@uri' -Rr <<< " $1 " ) " || return 1
# FIXME: No error handling if an error thrown here.
# TODO: Check if synapse ACTUALLY lets you do this. Docs say you can but who travis probably wrote them
curl -sf -X PUT -d '{"visibility":"private"}' \
-H "Content-Type: application/json" \
-H " Authorization: Bearer $ACCESS_TOKEN " " $BASE_URL /client/r0/directory/list/room/ $room_id "
return $?
}
deployRoomQuarantine ( ) {
# deployRoomQuarantine: room_id action
[ -n " $DRY_RUN " ] && return 0
local room_id
room_id = " $( jq '@uri' -Rr <<< " $1 " ) " || return 1
# FIXME: No error handling if an error thrown here.
curl -sf -X POST -d '{}' -H " Authorization: Bearer $ACCESS_TOKEN " " $SYNAPSE_BASE_URL /admin/v1/room/ $room_id /media/quarantine "
return $?
}
deployRoomRule ( ) {
# deployRoomRule: room_id action
local action_taken
local code
# TODO: Does synapse delist if shutting down? If so we should just skip delist if shutdown is defined
if jq -e '.room.delist' 1>/dev/null <<< " $2 " ; then
action_taken = '1'
deployRoomDelist " $1 " " $2 "
# TODO: Failure shouldn't block quarantine or shutdown, not the best way of handling.
#code="$?"
#[ "$code" -ne 0 ] && return $code
fi
if jq -e '.room.quarantine' 1>/dev/null <<< " $2 " ; then
action_taken = '1'
deployRoomQuarantine " $1 " " $2 "
code = " $? "
[ " $code " -ne 0 ] && return $code
fi
if jq -e '.room.shutdown' 1>/dev/null <<< " $2 " ; then
action_taken = '1'
deployRoomShutdown " $1 " " $2 "
code = " $? "
[ " $code " -ne 0 ] && return $code
fi
[ -z " $action_taken " ] && echo "deployRoomRule: Action defines no action. Odd" 2>& 1
return 0
}
deployUserRule ( ) {
# deployUserRule: user_id action
echo "deployUserRule: Not Implemented!" 1>& 2
return 255
}
deployRule ( ) {
# deployRule: rule action
local type
type = " $( jq .type -r <<< " $1 " ) "
if [ " $type " = = "m.user" ] ; then
deployUserRule " $1 " " $2 "
return $?
elif [ " $type " = = "m.room" ] ; then
deployRoomRule " $1 " " $2 "
return $?
else
die "deployRule: Internal logic error. Schema check let something slip through!"
fi
}
ingestRule ( ) {
# ingestRule: rule index state_key
local rule_tags
local actions_index
local action
local action_tags
local match
local code
rule_tags = " $( oneOfTagsAsArray <<< " $1 " | applyTopicsToTags) " || return 1
for actions_index in $( arrayIndexList " $ACTIONS " ) ; do
action = " $( jq " .[ $(( actions_index-1)) ] " -rc <<< " $ACTIONS " ) " || return 2
action_tags = " $( oneOfTagsAsArray <<< " $action " | applyTopicsToTags) " || return 3
match = " $( arrayAndIncludes " $rule_tags " " $action_tags " ) "
if [ -n " $match " ] ; then
echo " ingestRule[ $3 : $2 ]: Found matching tag \" $match \". "
deployRule " $1 " " $action "
code = " $? "
[ " $code " -ne 0 ] && echo " ingestRule[ $3 : $2 ]: Unexpected exit code of $code when handling action. " 1>& 2
return $code
fi
done
}
ingestPolicy ( ) {
# ingestPolicy: state state_key
local rule
local retcode = "0"
local rules
rules = " $( jq .content.rules -c <<< " $1 " ) "
for policy_index in $( arrayIndexList " $rules " ) ; do
rule = " $( jq " .[ $(( policy_index-1)) ] " -rc <<< " $rules " ) " || return 1
ingestRule " $rule " " $policy_index " " $2 "
local code = " $? "
if [ " $code " != "0" ] ; then
echo " ingestPolicy[ $policy_index ]: Unexpected exit code of $code when handling rule. "
local retcode = "2"
fi
done
return $retcode
}
ingestState ( ) {
# ingestState: state
local state_key
state_key = " $( stripCtrlChars <<< " $( jq .state_key -r <<< " $1 " ) " ) "
if ! checkPolicyIgnored " $1 " ; then
if checkPolicyAgainstSchema " $1 " ; then
if checkSignature " $1 " ; then
ingestPolicy " $1 " " $state_key " && genIgnorefileLine " $1 " >> " $IGNORED_FILE "
else
echo " ingestState[ $state_key ]: Unexpected GnuPG exit code $? . " 1>& 2
fi
else
echo " ingestState[ $state_key ]: Unexpected yajsv exit code $? . This policy is improperly formatted! " 1>& 2
genIgnorefileLine " $1 " >> " $IGNORED_FILE "
fi
else
echo " ingestState[ $state_key ]: Ignored. "
fi
}
ingestPolicyRoom ( ) {
# ingestPolicyRoom: room_id
local policies
local state
policies = " $( fetchRoomPolicies " $1 " ) " || return 1
for policies_index in $( arrayIndexList " $policies " ) ; do
state = " $( jq " .[ $(( policies_index-1)) ] " -rc <<< " $policies " ) " || return 2
ingestState " $state " || echo " ingestPolicyRoom[ $1 ]: Unexpected exit code of $? when processing state. " 1>& 2
done
}
init ( ) {
local room_id
local policy_rooms_index
for policy_rooms_index in $( arrayIndexList " $( jq '.policy_rooms' -c <<< " $CONFIG " ) " ) ; do
room_id = " $( jq " .policy_rooms[ $(( policy_rooms_index-1)) ] " -r <<< " $CONFIG " ) " || return 1
echo " main: Requesting policies for $room_id "
ingestPolicyRoom " $room_id " || die " init[ $room_id ]: Unexpected exit code when processing policy room. $? "
# processPolicyRoom "$room_id"
#code="$?"
#if [ "$code" != "0" ]; then echo "main: Unexpected exit code when processing policy room \"$room_id\": $code" 1>&2; fi
done
}
init