Add Nextcloud bad-signature recovery toolkit
This commit is contained in:
Executable
+956
@@ -0,0 +1,956 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CUR_IFS=$IFS
|
||||
|
||||
script_name="$(basename $(realpath $0))"
|
||||
script_dir="$(dirname $(realpath $0))"
|
||||
|
||||
conf_dir="${script_dir}/conf"
|
||||
snippet_dir="${script_dir}/snippets"
|
||||
report_dir="${script_dir}/reports"
|
||||
|
||||
declare -a unsorted_website_arr
|
||||
declare -a website_arr
|
||||
|
||||
declare -a unsorted_account_arr
|
||||
declare -a account_arr
|
||||
|
||||
declare -a unsorted_selected_user_arr
|
||||
declare -a selected_user_arr
|
||||
|
||||
LOCK_DIR="/tmp/${script_name%%.*}.LOCK"
|
||||
log_file="${LOCK_DIR}/${script_name%%.*}.log"
|
||||
|
||||
run_date=$(date +%Y-%m-%d-%H%M)
|
||||
|
||||
|
||||
# =============
|
||||
# --- Some functions
|
||||
# =============
|
||||
|
||||
usage() {
|
||||
|
||||
[[ -n "$1" ]] && error "$1"
|
||||
|
||||
[[ $terminal ]] && echo -e "
|
||||
\033[1mUsage:\033[m
|
||||
|
||||
$(basename $0) -s <website>
|
||||
|
||||
\033[1mDescription\033[m
|
||||
|
||||
READ-ONLY diagnostic for the 'Cannot decrypt this file, probably
|
||||
this is a shared file. Please ask the file owner to reshare the
|
||||
file with you.' and 'OCA\\Encryption\\Exceptions\\MultiKeyDecryptException'
|
||||
errors that recover_bad_signature.sh reports as READ_ERROR (and
|
||||
restore_bad_signature.sh can separately hit as WRITE_ERROR, even for
|
||||
files that read back fine during recovery) and neither script can
|
||||
fix - these are NOT signature problems, they are Server-Side
|
||||
Encryption KEY-MATERIAL problems (a per-user 'share key' for a file
|
||||
is either missing on disk or fails to decrypt with that user's
|
||||
private key). This script accepts either a recovery_*.tsv or a
|
||||
restore_*.tsv report as input.
|
||||
|
||||
This script changes NOTHING. It never decrypts a file, never
|
||||
touches 'encryption_skip_signature_check', and never writes
|
||||
anywhere except its own report file. It only:
|
||||
|
||||
1. Reads a chosen recovery_*.tsv report (produced by
|
||||
recover_bad_signature.sh) and picks out every row whose
|
||||
validation is READ_ERROR and whose detail text matches one of
|
||||
the two known key-material error signatures above.
|
||||
2. For each such file, resolves - via Nextcloud's normal,
|
||||
read-only Files API (\$node->getOwner(), \$folder->getById())
|
||||
- who currently owns the file, i.e. whether the affected
|
||||
account is the owner or only a share recipient.
|
||||
3. Checks, directly and only via plain filesystem existence
|
||||
tests (no content is read), whether the expected
|
||||
Server-Side-Encryption key files are present on disk:
|
||||
- the affected user's OWN private/public keypair
|
||||
(files_encryption/OC_DEFAULT_MODULE/<uid>.{private,public}Key)
|
||||
- the file's 'shareKey' entry for that user, under the
|
||||
file's OWNER's files_encryption tree
|
||||
4. Classifies every diagnosed file into one of:
|
||||
NO_OWN_KEYPAIR - the affected user has no encryption
|
||||
keypair on disk at all. This explains
|
||||
EVERY file failing for that user, not
|
||||
just this one - the account's own key
|
||||
material is broken/missing, independent
|
||||
of any single file or share.
|
||||
OWNER_UNRESOLVED - the file's current owner could not be
|
||||
determined via the Files API (e.g. the
|
||||
file/share no longer exists as such).
|
||||
NO_SHAREKEY - the owner could be resolved, and that
|
||||
user HAS their own keypair, but no
|
||||
shareKey file exists for them on this
|
||||
specific file.
|
||||
KEY_MATERIAL_PRESENT - both the user's keypair and this
|
||||
file's shareKey exist on disk. The
|
||||
failure is then a genuine
|
||||
decrypt/mismatch problem for this
|
||||
specific key (as with the
|
||||
MultiKeyDecryptException case), not a
|
||||
missing-key problem.
|
||||
|
||||
Read the report's per-account summary first: if an account shows
|
||||
NO_OWN_KEYPAIR for (almost) all its files, that account's own
|
||||
encryption keypair is the thing to fix/restore, not the individual
|
||||
files.
|
||||
|
||||
\033[1mOptions\033[m
|
||||
|
||||
-s <website>
|
||||
The site of the nextcloud instance.
|
||||
|
||||
\033[1mExample:\033[m
|
||||
|
||||
$(basename $0) -s cloud-02.oopen.de
|
||||
"
|
||||
|
||||
clean_up 1
|
||||
|
||||
}
|
||||
|
||||
|
||||
clean_up() {
|
||||
|
||||
# Perform program exit housekeeping
|
||||
[[ -n "$diag_php_file" ]] && rm -f "$diag_php_file" 2> /dev/null
|
||||
rm -rf "$LOCK_DIR"
|
||||
blank_line
|
||||
exit $1
|
||||
|
||||
}
|
||||
|
||||
is_number() {
|
||||
|
||||
return $(test ! -z "${1##*[!0-9]*}" > /dev/null 2>&1);
|
||||
}
|
||||
|
||||
echononl(){
|
||||
if $terminal ; then
|
||||
echo X\\c > /tmp/shprompt$$
|
||||
if [ `wc -c /tmp/shprompt$$ | awk '{print $1}'` -eq 1 ]; then
|
||||
echo -e -n "$*\\c" 1>&2
|
||||
else
|
||||
echo -e -n "$*" 1>&2
|
||||
fi
|
||||
rm /tmp/shprompt$$
|
||||
fi
|
||||
}
|
||||
echo_done() {
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[32mdone\033[m ]"
|
||||
fi
|
||||
}
|
||||
echo_ok() {
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[32mok\033[m ]"
|
||||
fi
|
||||
}
|
||||
echo_warning() {
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[33m\033[1mwarn\033[m ]"
|
||||
fi
|
||||
}
|
||||
echo_failed(){
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[1;31mfailed\033[m ]"
|
||||
fi
|
||||
}
|
||||
echo_skipped() {
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[37mskipped\033[m ]"
|
||||
fi
|
||||
}
|
||||
|
||||
fatal (){
|
||||
echo ""
|
||||
echo ""
|
||||
if $terminal ; then
|
||||
echo -e " [ \033[31m\033[1mFatal\033[m ]: \033[37m\033[1m$*\033[m"
|
||||
echo ""
|
||||
echo -e " \033[31m\033[1mScript will be interrupted..\033[m!"
|
||||
else
|
||||
echo " [ Fatal ]: $*"
|
||||
echo ""
|
||||
echo " Script was terminated...."
|
||||
fi
|
||||
clean_up 1
|
||||
}
|
||||
|
||||
error(){
|
||||
echo ""
|
||||
if $terminal ; then
|
||||
echo -e " [ \033[31m\033[1mError\033[m ]: $*"
|
||||
else
|
||||
echo " [ Error ]: $*"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
warn (){
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
echo -e " [ \033[33m\033[1mWarning\033[m ]: $*"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
info (){
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
echo -e " [ \033[32m\033[1mInfo\033[m ]: $*"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# - Remove leading/trailling whitespaces
|
||||
# -
|
||||
trim() {
|
||||
local var="$*"
|
||||
var="${var#"${var%%[![:space:]]*}"}" # remove leading whitespace characters
|
||||
var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters
|
||||
echo -n "$var"
|
||||
}
|
||||
|
||||
## - Check if a given array (parameter 2) contains a given string (parameter 1)
|
||||
## -
|
||||
containsElement () {
|
||||
local e
|
||||
for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
## - Percentage (1 decimal) of parameter 1 (part) against parameter 2 (total)
|
||||
## - Returns "0.0" if total is empty, zero or non-numeric.
|
||||
## -
|
||||
calc_percent() {
|
||||
local _part="$1"
|
||||
local _total="$2"
|
||||
if [[ -z "$_total" ]] || ! [[ "$_total" =~ ^[0-9]+$ ]] || [[ "$_total" -eq 0 ]] ; then
|
||||
echo "0.0"
|
||||
else
|
||||
awk -v b="$_part" -v t="$_total" 'BEGIN { printf "%.1f", (b/t)*100 }'
|
||||
fi
|
||||
}
|
||||
|
||||
blank_line() {
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
# - Running in a terminal?
|
||||
# -
|
||||
if [[ -t 1 ]] ; then
|
||||
terminal=true
|
||||
else
|
||||
terminal=false
|
||||
fi
|
||||
|
||||
# - This script needs root privileges (reads other users' key files
|
||||
# - under the Nextcloud data directory, 'su' into the webserver user
|
||||
# - to resolve file ownership through the normal Files API).
|
||||
# -
|
||||
if [[ "$(id -u)" -ne 0 ]] ; then
|
||||
fatal "This script must be run as root (it needs to read the Nextcloud data directory and 'su' into the webserver user). Please re-run as root, e.g. via sudo."
|
||||
fi
|
||||
|
||||
# ----------
|
||||
# - Jobhandling
|
||||
# ----------
|
||||
|
||||
if pgrep -f "$(basename $0)" | grep -q -v $$ ; then
|
||||
|
||||
msg="A previos instance of script \"`basename $0`\" seems already be running."
|
||||
|
||||
echo ""
|
||||
if $terminal ; then
|
||||
echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg"
|
||||
echo ""
|
||||
echo -e " \033[31m\033[1mScript was interupted\033[m!"
|
||||
else
|
||||
echo " [ Fatal ]: $msg"
|
||||
echo ""
|
||||
echo " Script was interupted!"
|
||||
fi
|
||||
echo
|
||||
|
||||
exit 1
|
||||
else
|
||||
if [[ -d "$LOCK_DIR" ]] ; then
|
||||
rm -rf "$LOCK_DIR" 2> /dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# - If job already runs, stop execution..
|
||||
# -
|
||||
if mkdir "$LOCK_DIR" 2> /dev/null ; then
|
||||
|
||||
# - Remove lockdir when the script finishes, or when it receives a signal
|
||||
# -
|
||||
trap clean_up SIGHUP SIGINT SIGTERM
|
||||
|
||||
else
|
||||
|
||||
msg="A previos instance of script \"`basename $0`\" seems already be running."
|
||||
|
||||
echo ""
|
||||
if $terminal ; then
|
||||
echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg"
|
||||
echo ""
|
||||
echo -e " \033[31m\033[1mScript was interupted\033[m!"
|
||||
else
|
||||
echo " [ Fatal ]: $msg"
|
||||
echo ""
|
||||
echo " Script was interupted!"
|
||||
fi
|
||||
echo
|
||||
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
|
||||
# -------------
|
||||
# - Read in Commandline arguments
|
||||
# -------------
|
||||
while getopts hs: opt ; do
|
||||
case $opt in
|
||||
h) usage ;;
|
||||
s) WEBSITE=$OPTARG ;;
|
||||
\?) usage
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
if [[ -z "$WEBSITE" ]] ; then
|
||||
|
||||
while IFS='' read -r -d '' _conf_file ; do
|
||||
source $_conf_file
|
||||
if [[ -n "$WEBSITE" ]] ; then
|
||||
unsorted_website_arr+=("${WEBSITE}:$_conf_file")
|
||||
fi
|
||||
WEBSITE=""
|
||||
done < <(find "${conf_dir}" -maxdepth 1 -type f -name "*.conf" -print0)
|
||||
|
||||
# - Sort array
|
||||
# -
|
||||
IFS=$'\n' website_arr=($(sort <<<"${unsorted_website_arr[*]}"))
|
||||
|
||||
# Which cloud instance (website) would you like to update
|
||||
#
|
||||
source ${snippet_dir}/get-cloud-instance-to-update.sh
|
||||
|
||||
else
|
||||
|
||||
while IFS='' read -r -d '' _conf_file ; do
|
||||
if $(grep -E -q "WEBSITE=\"?${WEBSITE}\"?" ${_conf_file} 2> /dev/null) ; then
|
||||
conf_file="${_conf_file}"
|
||||
break
|
||||
fi
|
||||
done < <(find "${conf_dir}" -maxdepth 1 -type f -name "*.conf" -print0)
|
||||
|
||||
fi
|
||||
|
||||
|
||||
# - Reset IFS
|
||||
# -
|
||||
IFS=$CUR_IFS
|
||||
|
||||
DEFAULT_SRC_BASE_DIR="/usr/local/src/nextcloud"
|
||||
DEFAULT_HTTP_USER="www-data"
|
||||
DEFAULT_HTTP_GROUP="www-data"
|
||||
DEFAULT_PHP_ENGINE='FPM'
|
||||
|
||||
blank_line
|
||||
echononl " Include Configuration file '$(basename "${conf_file}")'.."
|
||||
if [[ ! -f $conf_file ]]; then
|
||||
echo_skipped
|
||||
fatal "Missing configuration file '$conf_file'."
|
||||
else
|
||||
source $conf_file
|
||||
echo_ok
|
||||
fi
|
||||
|
||||
DEFAULT_WEB_BASE_DIR="/var/www/${WEBSITE}"
|
||||
[[ -n "$WEB_BASE_DIR" ]] || WEB_BASE_DIR=$DEFAULT_WEB_BASE_DIR
|
||||
|
||||
if [[ ! -d ${WEB_BASE_DIR} ]] ; then
|
||||
fatal "Web base directory '$WEB_BASE_DIR' not found!"
|
||||
fi
|
||||
|
||||
DATA_DIR="$(realpath ${WEB_BASE_DIR}/data)"
|
||||
|
||||
[[ -n "$PHP_ENGINE" ]] || PHP_ENGINE=$DEFAULT_PHP_ENGINE
|
||||
|
||||
INSTALL_DIR="$(realpath ${WEB_BASE_DIR}/nextcloud)"
|
||||
CURRENT_VERSION="$(basename $INSTALL_DIR | cut -d"-" -f2)"
|
||||
|
||||
|
||||
# =============
|
||||
# --- Some
|
||||
# =============
|
||||
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
echo -e "\033[32m-----\033[m"
|
||||
echo -e "Diagnose Server-Side-Encryption \033[1mkey-material\033[m problems on system \033[1m${WEB_BASE_DIR}\033[m"
|
||||
echo -e "\033[1m
|
||||
READ-ONLY: this script never decrypts anything, never writes to
|
||||
Nextcloud storage and never touches 'encryption_skip_signature_check'.
|
||||
It only inspects, on disk and via the normal Files API, whether the
|
||||
expected encryption key files are present for accounts/files that
|
||||
recover_bad_signature.sh could not read (READ_ERROR, not 'Bad
|
||||
Signature').\033[m"
|
||||
echo -e "\033[32m-----\033[m"
|
||||
fi
|
||||
|
||||
|
||||
# =============
|
||||
# --- Some checks
|
||||
# =============
|
||||
|
||||
DEFAULT_HTTP_USER="www-data"
|
||||
DEFAULT_HTTP_GROUP="www-data"
|
||||
|
||||
NGINX_IS_ENABLED=false
|
||||
APACHE2_IS_ENABLED=false
|
||||
|
||||
systemd=$(which systemd)
|
||||
systemctl=$(which systemctl)
|
||||
|
||||
# Get Webservice environment as IS_HTTPD_RUNNING, HTTP_USER, HTTP_GROUP..
|
||||
#
|
||||
source ${snippet_dir}/get-webservice-environment.sh
|
||||
|
||||
|
||||
# Check PHP Version
|
||||
#
|
||||
source ${snippet_dir}/get-php-major-version.sh
|
||||
|
||||
|
||||
# Get full qualified PHP command
|
||||
#
|
||||
source ${snippet_dir}/get-path-of-php-command.sh
|
||||
|
||||
|
||||
if [[ ! -x "$PHP_BIN" ]]; then
|
||||
fatal "No PHP binary found!"
|
||||
fi
|
||||
|
||||
|
||||
# =============
|
||||
# --- Determine available reports to diagnose from - either a
|
||||
# --- recover_bad_signature.sh report (recovery_*.tsv, READ_ERROR rows)
|
||||
# --- or a restore_bad_signature.sh report (restore_*.tsv, WRITE_ERROR
|
||||
# --- rows) - both can carry the same key-material error signatures,
|
||||
# --- just at a different step (read vs. write).
|
||||
# =============
|
||||
|
||||
mapfile -t _available_reports < <(ls -t "${report_dir}"/recovery_*.tsv "${report_dir}"/restore_*.tsv 2> /dev/null)
|
||||
|
||||
if [[ ${#_available_reports[@]} -eq 0 ]] ; then
|
||||
fatal "No recovery_*.tsv or restore_*.tsv report files found in '${report_dir}'. Run recover_bad_signature.sh (and/or restore_bad_signature.sh) first."
|
||||
fi
|
||||
|
||||
blank_line
|
||||
if $terminal ; then
|
||||
echo -e "\033[37m\033[1mAvailable reports (newest first, recovery_*.tsv and restore_*.tsv)\033[m"
|
||||
echo ""
|
||||
_i=1
|
||||
for _f in "${_available_reports[@]}" ; do
|
||||
printf " [%2d] %s\n" "$_i" "$(basename "$_f")"
|
||||
(( _i++ ))
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo -n " Select report by number: "
|
||||
read _report_choice
|
||||
|
||||
if ! [[ "$_report_choice" =~ ^[0-9]+$ ]] || [[ "$_report_choice" -lt 1 ]] || [[ "$_report_choice" -gt ${#_available_reports[@]} ]] ; then
|
||||
fatal "Invalid selection '$_report_choice'."
|
||||
fi
|
||||
|
||||
chosen_report="${_available_reports[$((_report_choice-1))]}"
|
||||
|
||||
|
||||
# =============
|
||||
# --- Extract "user<TAB>path<TAB>detail" for every row whose detail
|
||||
# --- text matches one of the two known key-material error signatures
|
||||
# --- - a recovery report's READ_ERROR rows (6 tab-separated fields,
|
||||
# --- validation in $5) as well as a restore report's WRITE_ERROR rows
|
||||
# --- (4 tab-separated fields, status in $3) both match: the same
|
||||
# --- underlying key-material problem can surface at either step. Same
|
||||
# --- section-header state machine as restore_bad_signature.sh uses
|
||||
# --- for VALID rows.
|
||||
# =============
|
||||
|
||||
key_error_entries_file="${LOCK_DIR}/key_error_entries.tsv"
|
||||
awk -F'\t' '
|
||||
/^-+$/ {
|
||||
if (state == 0) { state = 1 }
|
||||
else if (state == 2) { state = 0 }
|
||||
next
|
||||
}
|
||||
{
|
||||
if (state == 1) { user = $0; state = 2; next }
|
||||
if (NF == 6 && $1 != "path" && $5 == "READ_ERROR") {
|
||||
if (index($6, "probably this is a shared file") > 0 || index($6, "MultiKeyDecryptException") > 0) {
|
||||
print user "\t" $1 "\t" $6
|
||||
}
|
||||
} else if (NF == 4 && $1 != "path" && $3 == "WRITE_ERROR") {
|
||||
if (index($4, "probably this is a shared file") > 0 || index($4, "MultiKeyDecryptException") > 0) {
|
||||
print user "\t" $1 "\t" $4
|
||||
}
|
||||
}
|
||||
}
|
||||
' "$chosen_report" > "$key_error_entries_file"
|
||||
|
||||
unsorted_account_arr=($(cut -f1 "$key_error_entries_file" | sort -u))
|
||||
IFS=$'\n' account_arr=($(sort <<<"${unsorted_account_arr[*]}"))
|
||||
IFS=$CUR_IFS
|
||||
|
||||
if [[ ${#account_arr[@]} -eq 0 ]] ; then
|
||||
fatal "No key-material READ_ERROR/WRITE_ERROR entries ('...probably this is a shared file...' or 'MultiKeyDecryptException') found in '$(basename "$chosen_report")' - nothing to diagnose."
|
||||
fi
|
||||
|
||||
blank_line
|
||||
if $terminal ; then
|
||||
echo -e "\033[37m\033[1mAccounts with key-material READ_ERROR entries in this report\033[m"
|
||||
echo ""
|
||||
for _a in "${account_arr[@]}" ; do
|
||||
_a_count=$(awk -F'\t' -v u="$_a" '$1==u' "$key_error_entries_file" | wc -l)
|
||||
echo " - $_a (${_a_count})"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo -n " Account(s) to diagnose (blank separated, or 'all'): "
|
||||
read _input
|
||||
|
||||
while true ; do
|
||||
|
||||
IFS=' ' read -r -a unsorted_selected_user_arr <<< "$(trim "$_input")"
|
||||
|
||||
if [[ "${#unsorted_selected_user_arr[@]}" -eq 1 && "${unsorted_selected_user_arr[0],,}" = "all" ]] ; then
|
||||
selected_user_arr=("${account_arr[@]}")
|
||||
break
|
||||
fi
|
||||
|
||||
_all_valid=true
|
||||
for _u in "${unsorted_selected_user_arr[@]}" ; do
|
||||
if ! containsElement "$_u" "${account_arr[@]}" ; then
|
||||
error "Unknown account '$_u' (not found in the selected report)."
|
||||
_all_valid=false
|
||||
fi
|
||||
done
|
||||
|
||||
if $_all_valid && [[ "${#unsorted_selected_user_arr[@]}" -gt 0 ]] ; then
|
||||
IFS=$'\n' selected_user_arr=($(sort <<<"${unsorted_selected_user_arr[*]}"))
|
||||
IFS=$CUR_IFS
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n " Account(s) to diagnose (blank separated, or 'all'): "
|
||||
read _input
|
||||
|
||||
done
|
||||
|
||||
|
||||
mkdir -p "$report_dir" 2> /dev/null
|
||||
diag_report_file="${report_dir}/diagnose_share_key_${WEBSITE}_${run_date}.tsv"
|
||||
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
echo -e "\033[1;32mStarting key-material diagnosis for \033[1;37m${WEBSITE}\033[m"
|
||||
echo ""
|
||||
echo -e " Cloud instance..........................: $WEBSITE"
|
||||
echo -e " Recovery report used....................: $(basename "$chosen_report")"
|
||||
echo -e " Account(s) to diagnose..................: \033[33m${selected_user_arr[*]}\033[m"
|
||||
echo -e " Diagnose report..........................: reports/$(basename "${diag_report_file}")"
|
||||
echo ""
|
||||
info "Read-only - nothing is decrypted, written or changed anywhere."
|
||||
fi
|
||||
|
||||
|
||||
{
|
||||
echo "=================================================================="
|
||||
echo " Share-Key-Diagnose ${WEBSITE} ${run_date}"
|
||||
echo "=================================================================="
|
||||
echo ""
|
||||
echo " Basis-Report (Recovery): $(basename "$chosen_report")"
|
||||
echo " Diagnostiziert werden alle READ_ERROR-Eintraege mit den Fehlerbildern"
|
||||
echo " 'probably this is a shared file' und 'MultiKeyDecryptException'."
|
||||
echo " READ-ONLY: nichts wird entschluesselt, geschrieben oder veraendert."
|
||||
echo ""
|
||||
printf 'path\towner\thas_own_keypair\thas_filekey\thas_sharekey\tclassification\toriginal_error\n'
|
||||
} > "$diag_report_file"
|
||||
|
||||
|
||||
# =============
|
||||
# --- Write the embedded PHP diagnosis helper (read-only: resolves the
|
||||
# --- current owner of each file via the normal Files API)
|
||||
# =============
|
||||
|
||||
diag_php_file="${INSTALL_DIR}/.diagnose_share_key_$$.php"
|
||||
|
||||
cat > "$diag_php_file" <<'PHP_DIAG_EOF'
|
||||
<?php
|
||||
/**
|
||||
* diagnose_share_key.php
|
||||
*
|
||||
* READ-ONLY. For each given path, resolves (via the normal, standard
|
||||
* Files API) who currently owns the file and - if the owner differs
|
||||
* from the requesting account - the owner's own path to that same
|
||||
* file (via Folder::getById(), a stable public API), so the caller
|
||||
* can look up the file's on-disk encryption key material under the
|
||||
* OWNER's files_encryption tree. Nothing is read, decrypted or
|
||||
* written beyond this metadata.
|
||||
*
|
||||
* Usage: php diagnose_share_key.php <uid> <listFile>
|
||||
* listFile: one path per line
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
define('OC_CONSOLE', 1);
|
||||
|
||||
$oldWorkingDir = getcwd();
|
||||
if ($oldWorkingDir === false) {
|
||||
fwrite(STDERR, "Konnte aktuelles Arbeitsverzeichnis nicht ermitteln. Bitte mit absolutem Pfad aufrufen.\n");
|
||||
exit(1);
|
||||
}
|
||||
chdir(__DIR__);
|
||||
require_once __DIR__ . '/lib/base.php';
|
||||
chdir($oldWorkingDir);
|
||||
|
||||
if (function_exists('posix_getuid') && posix_getuid() === 0) {
|
||||
fwrite(STDERR, "Bitte NICHT als root ausfuehren, sondern als Webserver-User.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
$uid = $argv[1] ?? null;
|
||||
$listFile = $argv[2] ?? null;
|
||||
|
||||
if ($uid === null || $listFile === null) {
|
||||
fwrite(STDERR, "Usage: php diagnose_share_key.php <uid> <listFile>\n");
|
||||
exit(1);
|
||||
}
|
||||
if (!is_file($listFile)) {
|
||||
fwrite(STDERR, "Liste nicht gefunden: $listFile\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$rootFolder = \OC::$server->get(\OCP\Files\IRootFolder::class);
|
||||
\OC_Util::setupFS($uid);
|
||||
|
||||
$lines = file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if ($lines === false) {
|
||||
$lines = [];
|
||||
}
|
||||
|
||||
echo "path\towner\townerpath\tstatus\tdetail\n";
|
||||
|
||||
foreach ($lines as $path) {
|
||||
if ($path === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$node = $rootFolder->get($path);
|
||||
} catch (\Throwable $e) {
|
||||
$msg = str_replace(["\t", "\n", "\r"], ' ', $e->getMessage());
|
||||
echo "$path\t?\t?\tUNRESOLVED\t" . get_class($e) . ": $msg\n";
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
|
||||
$ownerUid = null;
|
||||
try {
|
||||
$owner = $node->getOwner();
|
||||
$ownerUid = $owner ? $owner->getUID() : null;
|
||||
} catch (\Throwable $e) {
|
||||
$ownerUid = null;
|
||||
}
|
||||
|
||||
if ($ownerUid === null) {
|
||||
echo "$path\t?\t?\tOWNER_UNRESOLVED\tgetOwner() returned no owner\n";
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
|
||||
$ownerPath = $path;
|
||||
if ($ownerUid !== $uid) {
|
||||
try {
|
||||
$ownerNodes = $rootFolder->getUserFolder($ownerUid)->getById($node->getId());
|
||||
if (!empty($ownerNodes)) {
|
||||
$ownerPath = $ownerNodes[0]->getPath();
|
||||
} else {
|
||||
echo "$path\t$ownerUid\t?\tOWNER_UNRESOLVED\tfile not found in owner's ($ownerUid) own tree via getById()\n";
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$msg = str_replace(["\t", "\n", "\r"], ' ', $e->getMessage());
|
||||
echo "$path\t$ownerUid\t?\tOWNER_UNRESOLVED\t" . get_class($e) . ": $msg\n";
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
echo "$path\t$ownerUid\t$ownerPath\tOK\t\n";
|
||||
flush();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, "\n!!! UNBEHANDELTE AUSNAHME !!!\n");
|
||||
fwrite(STDERR, get_class($e) . ": " . $e->getMessage() . "\n");
|
||||
fwrite(STDERR, "in " . $e->getFile() . ":" . $e->getLine() . "\n");
|
||||
fwrite(STDERR, $e->getTraceAsString() . "\n");
|
||||
exit(2);
|
||||
}
|
||||
PHP_DIAG_EOF
|
||||
|
||||
chmod 644 "$diag_php_file"
|
||||
|
||||
|
||||
# -----
|
||||
# - Main part of the script
|
||||
# -----
|
||||
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
echo ""
|
||||
echo -e "\033[37m\033[1mMain part of the script\033[m"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
declare -i total_selected=0
|
||||
declare -i total_no_own_keypair=0
|
||||
declare -i total_owner_unresolved=0
|
||||
declare -i total_no_sharekey=0
|
||||
declare -i total_no_filekey=0
|
||||
declare -i total_key_material_present=0
|
||||
declare -i total_failed_users=0
|
||||
|
||||
for _user in "${selected_user_arr[@]}" ; do
|
||||
|
||||
_sep_len=${#_user}
|
||||
[[ $_sep_len -lt 9 ]] && _sep_len=9
|
||||
_sep_line="$(printf '%*s' "$_sep_len" '' | tr ' ' '-')"
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo ""
|
||||
echo "$_sep_line"
|
||||
echo "$_user"
|
||||
echo "$_sep_line"
|
||||
} >> "$diag_report_file"
|
||||
|
||||
# - This account's own keypair - a single, cheap, bash-only check
|
||||
# - that alone explains an entire account failing on EVERY file, if
|
||||
# - it is missing.
|
||||
# -
|
||||
_user_privkey="${DATA_DIR}/${_user}/files_encryption/OC_DEFAULT_MODULE/${_user}.privateKey"
|
||||
_user_pubkey="${DATA_DIR}/${_user}/files_encryption/OC_DEFAULT_MODULE/${_user}.publicKey"
|
||||
if [[ -f "$_user_privkey" && -f "$_user_pubkey" ]] ; then
|
||||
_user_has_own_keypair=true
|
||||
else
|
||||
_user_has_own_keypair=false
|
||||
fi
|
||||
|
||||
path_list_file="${LOCK_DIR}/paths_${_user}.txt"
|
||||
awk -F'\t' -v u="$_user" '$1==u { print $2 }' "$key_error_entries_file" > "$path_list_file"
|
||||
|
||||
declare -i _user_selected=0
|
||||
_user_selected=$(wc -l < "$path_list_file")
|
||||
|
||||
owner_result_tsv="${LOCK_DIR}/owner_${_user}.tsv"
|
||||
|
||||
echononl " Resolving ownership for account \033[1;37m${_user}\033[m (${_user_selected} files).."
|
||||
su -c "$PHP_BIN $diag_php_file $_user $path_list_file" -s /bin/bash $HTTP_USER > "$owner_result_tsv" 2> "$log_file"
|
||||
_rc=$?
|
||||
|
||||
if [[ $_rc -ne 0 ]]; then
|
||||
echo_failed
|
||||
error "$(cat "$log_file")"
|
||||
echo " [ FEHLER bei der Eigentuemer-Aufloesung - siehe Server-Log ]" >> "$diag_report_file"
|
||||
(( total_failed_users++ ))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo_done
|
||||
|
||||
declare -i _user_no_own_keypair=0
|
||||
declare -i _user_owner_unresolved=0
|
||||
declare -i _user_no_sharekey=0
|
||||
declare -i _user_no_filekey=0
|
||||
declare -i _user_key_material_present=0
|
||||
declare -a _user_lines=()
|
||||
|
||||
while IFS=$'\t' read -r _path _owner _ownerpath _status _odetail ; do
|
||||
|
||||
[[ "$_path" = "path" ]] && continue
|
||||
[[ -z "$_path" ]] && continue
|
||||
|
||||
_orig_error="$(awk -F'\t' -v u="$_user" -v p="$_path" '$1==u && $2==p { print $3; exit }' "$key_error_entries_file")"
|
||||
|
||||
if ! $_user_has_own_keypair ; then
|
||||
(( _user_no_own_keypair++ ))
|
||||
_class="NO_OWN_KEYPAIR"
|
||||
_detail="account '${_user}' has no encryption keypair on disk at all (missing: ${_user_privkey} and/or ${_user_pubkey}) - this explains every file failing for this account, independent of sharing"
|
||||
_user_lines+=("${_path}"$'\t'"${_class}: ${_detail}")
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_path" "${_owner:-?}" "no" "n/a" "n/a" "$_class" "$_orig_error" >> "$diag_report_file"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$_status" != "OK" ]] ; then
|
||||
(( _user_owner_unresolved++ ))
|
||||
_class="OWNER_UNRESOLVED"
|
||||
_detail="could not determine current owner/path via the Files API (${_status}: ${_odetail})"
|
||||
_user_lines+=("${_path}"$'\t'"${_class}: ${_detail}")
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_path" "${_owner:-?}" "yes" "n/a" "n/a" "$_class" "$_orig_error" >> "$diag_report_file"
|
||||
continue
|
||||
fi
|
||||
|
||||
_owner_rel="${_ownerpath#/${_owner}/files/}"
|
||||
|
||||
if [[ "$_owner_rel" = "$_ownerpath" ]] ; then
|
||||
# Prefix did not match - path shape was not what we expected.
|
||||
(( _user_owner_unresolved++ ))
|
||||
_class="OWNER_UNRESOLVED"
|
||||
_detail="owner path '${_ownerpath}' did not have the expected '/${_owner}/files/...' shape"
|
||||
_user_lines+=("${_path}"$'\t'"${_class}: ${_detail}")
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_path" "${_owner:-?}" "yes" "n/a" "n/a" "$_class" "$_orig_error" >> "$diag_report_file"
|
||||
continue
|
||||
fi
|
||||
|
||||
# - NOTE: an earlier version of this script also required a
|
||||
# - standalone 'fileKey' file next to the shareKey and treated
|
||||
# - its absence as a failure (NO_FILEKEY). That was WRONG: in
|
||||
# - the default (non-legacy, non-masterkey) key storage mode,
|
||||
# - Nextcloud's Encryption module never writes a plain 'fileKey'
|
||||
# - file at all - the file key only ever exists ENCRYPTED, once
|
||||
# - per user with access, as '<uid>.shareKey'; getFileKey()
|
||||
# - reconstructs it by decrypting one of those. A uniform
|
||||
# - 'fileKey missing' result across many unrelated accounts (as
|
||||
# - seen in practice) is the tell that this check was testing
|
||||
# - for something that legitimately never exists here, not a
|
||||
# - real defect - so it is now purely informational and never
|
||||
# - drives the classification.
|
||||
# -
|
||||
_filekey_path="${DATA_DIR}/${_owner}/files_encryption/keys/files/${_owner_rel}/OC_DEFAULT_MODULE/fileKey"
|
||||
_sharekey_path="${DATA_DIR}/${_owner}/files_encryption/keys/files/${_owner_rel}/OC_DEFAULT_MODULE/${_user}.shareKey"
|
||||
|
||||
if [[ -f "$_filekey_path" ]] ; then
|
||||
_has_filekey="yes"
|
||||
else
|
||||
_has_filekey="no (informational only - see note above; NOT used to classify)"
|
||||
fi
|
||||
if [[ -f "$_sharekey_path" ]] ; then
|
||||
_has_sharekey="yes"
|
||||
else
|
||||
_has_sharekey="no"
|
||||
fi
|
||||
|
||||
if [[ "$_has_sharekey" = "no" ]] ; then
|
||||
(( _user_no_sharekey++ ))
|
||||
_class="NO_SHAREKEY"
|
||||
_detail="no shareKey exists for '${_user}' on this file (${_sharekey_path}) - '${_user}' was probably never (re-)granted a usable key for this file"
|
||||
else
|
||||
(( _user_key_material_present++ ))
|
||||
_class="KEY_MATERIAL_PRESENT"
|
||||
_detail="'${_user}.shareKey' exists on disk (${_sharekey_path}) - the failure is a genuine decrypt/mismatch problem: '${_user}'s current private key cannot decrypt this existing shareKey (owner: ${_owner}$([[ "$_owner" != "$_user" ]] && echo ", shared file"))"
|
||||
fi
|
||||
|
||||
_user_lines+=("${_path}"$'\t'"${_class}: ${_detail}")
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$_path" "$_owner" "yes" "$_has_filekey" "$_has_sharekey" "$_class" "$_orig_error" >> "$diag_report_file"
|
||||
|
||||
done < "$owner_result_tsv"
|
||||
|
||||
if $terminal ; then
|
||||
echo -e " \033[1;37mAccount ${_user}\033[m"
|
||||
echo -e " Diagnostiziert.............................: ${_user_selected}"
|
||||
if [[ $_user_no_own_keypair -gt 0 ]] ; then
|
||||
echo -e " Kein eigenes Schluesselpaar (NO_OWN_KEYPAIR): \033[1;31m${_user_no_own_keypair}\033[m"
|
||||
fi
|
||||
[[ $_user_owner_unresolved -gt 0 ]] && echo -e " Eigentuemer nicht aufloesbar................: \033[33m${_user_owner_unresolved}\033[m"
|
||||
[[ $_user_no_filekey -gt 0 ]] && echo -e " Datei-Schluessel fehlt (NO_FILEKEY)........: \033[33m${_user_no_filekey}\033[m"
|
||||
[[ $_user_no_sharekey -gt 0 ]] && echo -e " Freigabe-Schluessel fehlt (NO_SHAREKEY)....: \033[33m${_user_no_sharekey}\033[m"
|
||||
[[ $_user_key_material_present -gt 0 ]] && echo -e " Schluessel vorhanden, Entschluesseln schlaegt fehl: \033[33m${_user_key_material_present}\033[m"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
{
|
||||
echo ""
|
||||
for _line in "${_user_lines[@]}" ; do
|
||||
printf ' %s\n' "$_line"
|
||||
done
|
||||
echo ""
|
||||
echo " Account ${_user}"
|
||||
echo " Diagnostiziert.............................: ${_user_selected}"
|
||||
[[ $_user_no_own_keypair -gt 0 ]] && echo " Kein eigenes Schluesselpaar (NO_OWN_KEYPAIR): ${_user_no_own_keypair}"
|
||||
[[ $_user_owner_unresolved -gt 0 ]] && echo " Eigentuemer nicht aufloesbar................: ${_user_owner_unresolved}"
|
||||
[[ $_user_no_filekey -gt 0 ]] && echo " Datei-Schluessel fehlt (NO_FILEKEY)........: ${_user_no_filekey}"
|
||||
[[ $_user_no_sharekey -gt 0 ]] && echo " Freigabe-Schluessel fehlt (NO_SHAREKEY)....: ${_user_no_sharekey}"
|
||||
[[ $_user_key_material_present -gt 0 ]] && echo " Schluessel vorhanden, Entschluesseln schlaegt fehl: ${_user_key_material_present}"
|
||||
} >> "$diag_report_file"
|
||||
|
||||
(( total_selected += _user_selected ))
|
||||
(( total_no_own_keypair += _user_no_own_keypair ))
|
||||
(( total_owner_unresolved += _user_owner_unresolved ))
|
||||
(( total_no_sharekey += _user_no_sharekey ))
|
||||
(( total_no_filekey += _user_no_filekey ))
|
||||
(( total_key_material_present += _user_key_material_present ))
|
||||
|
||||
unset _user_no_own_keypair _user_owner_unresolved _user_no_sharekey _user_no_filekey _user_key_material_present _user_lines
|
||||
|
||||
done
|
||||
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo ""
|
||||
echo "=================================================================="
|
||||
echo " Gesamtergebnis"
|
||||
echo "=================================================================="
|
||||
echo ""
|
||||
echo "Ausgewaehlte Accounts.......................: ${#selected_user_arr[@]}"
|
||||
[[ $total_failed_users -gt 0 ]] && echo "Accounts mit Diagnose-Fehler................: ${total_failed_users}"
|
||||
echo "Diagnostiziert insgesamt.....................: ${total_selected}"
|
||||
[[ $total_no_own_keypair -gt 0 ]] && echo "Kein eigenes Schluesselpaar (NO_OWN_KEYPAIR): ${total_no_own_keypair}"
|
||||
[[ $total_owner_unresolved -gt 0 ]] && echo "Eigentuemer nicht aufloesbar.................: ${total_owner_unresolved}"
|
||||
[[ $total_no_filekey -gt 0 ]] && echo "Datei-Schluessel fehlt (NO_FILEKEY).........: ${total_no_filekey}"
|
||||
[[ $total_no_sharekey -gt 0 ]] && echo "Freigabe-Schluessel fehlt (NO_SHAREKEY)......: ${total_no_sharekey}"
|
||||
[[ $total_key_material_present -gt 0 ]] && echo "Schluessel vorhanden, Entschluesseln schlaegt fehl: ${total_key_material_present}"
|
||||
echo ""
|
||||
echo "READ-ONLY: es wurde nichts entschluesselt, geschrieben oder veraendert."
|
||||
} >> "$diag_report_file"
|
||||
|
||||
blank_line
|
||||
|
||||
if $terminal ; then
|
||||
echo -e "\033[37m\033[1mErgebnis\033[m"
|
||||
echo ""
|
||||
echo -e " Ausgewaehlte Accounts.......................: ${#selected_user_arr[@]}"
|
||||
[[ $total_failed_users -gt 0 ]] && echo -e " Accounts mit Diagnose-Fehler................: \033[1;31m${total_failed_users}\033[m"
|
||||
echo -e " Diagnostiziert insgesamt.....................: ${total_selected}"
|
||||
[[ $total_no_own_keypair -gt 0 ]] && echo -e " Kein eigenes Schluesselpaar (NO_OWN_KEYPAIR): \033[1;31m${total_no_own_keypair}\033[m"
|
||||
[[ $total_owner_unresolved -gt 0 ]] && echo -e " Eigentuemer nicht aufloesbar.................: \033[33m${total_owner_unresolved}\033[m"
|
||||
[[ $total_no_filekey -gt 0 ]] && echo -e " Datei-Schluessel fehlt (NO_FILEKEY).........: \033[33m${total_no_filekey}\033[m"
|
||||
[[ $total_no_sharekey -gt 0 ]] && echo -e " Freigabe-Schluessel fehlt (NO_SHAREKEY)......: \033[33m${total_no_sharekey}\033[m"
|
||||
[[ $total_key_material_present -gt 0 ]] && echo -e " Schluessel vorhanden, Entschluesseln schlaegt fehl: \033[33m${total_key_material_present}\033[m"
|
||||
echo ""
|
||||
echo -e " Diagnose-Report...............................: reports/$(basename "${diag_report_file}")"
|
||||
echo ""
|
||||
info "Lies zuerst die Account-Zusammenfassung: 'NO_OWN_KEYPAIR' bei (fast) allen Dateien eines Accounts heisst, das eigene Schluesselpaar dieses Accounts ist das Problem - nicht die einzelnen Dateien."
|
||||
fi
|
||||
|
||||
clean_up 0
|
||||
Executable
+887
@@ -0,0 +1,887 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CUR_IFS=$IFS
|
||||
|
||||
script_name="$(basename $(realpath $0))"
|
||||
script_dir="$(dirname $(realpath $0))"
|
||||
|
||||
conf_dir="${script_dir}/conf"
|
||||
snippet_dir="${script_dir}/snippets"
|
||||
report_dir="${script_dir}/reports"
|
||||
|
||||
declare -a unsorted_website_arr
|
||||
declare -a website_arr
|
||||
|
||||
declare -a unsorted_account_arr
|
||||
declare -a account_arr
|
||||
|
||||
declare -a unsorted_selected_user_arr
|
||||
declare -a selected_user_arr
|
||||
|
||||
LOCK_DIR="/tmp/${script_name%%.*}.LOCK"
|
||||
log_file="${LOCK_DIR}/${script_name%%.*}.log"
|
||||
|
||||
run_date=$(date +%Y-%m-%d-%H%M)
|
||||
|
||||
|
||||
# =============
|
||||
# --- Some functions
|
||||
# =============
|
||||
|
||||
usage() {
|
||||
|
||||
[[ -n "$1" ]] && error "$1"
|
||||
|
||||
[[ $terminal ]] && echo -e "
|
||||
\033[1mUsage:\033[m
|
||||
|
||||
$(basename $0) -s <website>
|
||||
|
||||
\033[1mDescription\033[m
|
||||
|
||||
Scans the encrypted files of one, several or all Nextcloud accounts
|
||||
and reports every file that fails to decrypt (e.g. 'Bad Signature'
|
||||
errors thrown by Server-Side Encryption). Every file is read once
|
||||
and its content discarded - the script is READ-ONLY and changes,
|
||||
moves or deletes nothing.
|
||||
|
||||
You will be asked interactively which account(s) to scan:
|
||||
|
||||
- a blank separated list of account names
|
||||
- or 'all' to scan every existing account
|
||||
|
||||
\033[1mOptions\033[m
|
||||
|
||||
-s <website>
|
||||
The site of the nextcloud instance.
|
||||
|
||||
\033[1mExample:\033[m
|
||||
|
||||
Runs this script on system 'cloud-01.oopen.de'
|
||||
|
||||
$(basename $0) -s cloud-01.oopen.de
|
||||
"
|
||||
|
||||
clean_up 1
|
||||
|
||||
}
|
||||
|
||||
|
||||
clean_up() {
|
||||
|
||||
# Perform program exit housekeeping
|
||||
[[ -n "$scan_php_file" ]] && rm -f "$scan_php_file" 2> /dev/null
|
||||
rm -rf "$LOCK_DIR"
|
||||
blank_line
|
||||
exit $1
|
||||
}
|
||||
|
||||
is_number() {
|
||||
|
||||
return $(test ! -z "${1##*[!0-9]*}" > /dev/null 2>&1);
|
||||
|
||||
# - also possible
|
||||
# -
|
||||
#[[ ! -z "${1##*[!0-9]*}" ]] && return 0 || return 1
|
||||
#return $([[ ! -z "${1##*[!0-9]*}" ]])
|
||||
}
|
||||
|
||||
echononl(){
|
||||
if $terminal ; then
|
||||
echo X\\c > /tmp/shprompt$$
|
||||
if [ `wc -c /tmp/shprompt$$ | awk '{print $1}'` -eq 1 ]; then
|
||||
echo -e -n "$*\\c" 1>&2
|
||||
else
|
||||
echo -e -n "$*" 1>&2
|
||||
fi
|
||||
rm /tmp/shprompt$$
|
||||
fi
|
||||
}
|
||||
echo_done() {
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[32mdone\033[m ]"
|
||||
fi
|
||||
}
|
||||
echo_ok() {
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[32mok\033[m ]"
|
||||
fi
|
||||
}
|
||||
echo_warning() {
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[33m\033[1mwarn\033[m ]"
|
||||
fi
|
||||
}
|
||||
echo_failed(){
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[1;31mfailed\033[m ]"
|
||||
fi
|
||||
}
|
||||
echo_skipped() {
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[37mskipped\033[m ]"
|
||||
fi
|
||||
}
|
||||
echo_funde() {
|
||||
if $terminal ; then
|
||||
echo -e "\033[75G[ \033[33m\033[1mFunde!\033[m ]"
|
||||
fi
|
||||
}
|
||||
|
||||
fatal (){
|
||||
echo ""
|
||||
echo ""
|
||||
if $terminal ; then
|
||||
echo -e " [ \033[31m\033[1mFatal\033[m ]: \033[37m\033[1m$*\033[m"
|
||||
echo ""
|
||||
echo -e " \033[31m\033[1mScript will be interrupted..\033[m!"
|
||||
else
|
||||
echo " [ Fatal ]: $*"
|
||||
echo ""
|
||||
echo " Script was terminated...."
|
||||
fi
|
||||
clean_up 1
|
||||
}
|
||||
|
||||
error(){
|
||||
echo ""
|
||||
if $terminal ; then
|
||||
echo -e " [ \033[31m\033[1mError\033[m ]: $*"
|
||||
else
|
||||
echo " [ Error ]: $*"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
warn (){
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
echo -e " [ \033[33m\033[1mWarning\033[m ]: $*"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
info (){
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
echo -e " [ \033[32m\033[1mInfo\033[m ]: $*"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# - Remove leading/trailling whitespaces
|
||||
# -
|
||||
trim() {
|
||||
local var="$*"
|
||||
var="${var#"${var%%[![:space:]]*}"}" # remove leading whitespace characters
|
||||
var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters
|
||||
echo -n "$var"
|
||||
}
|
||||
|
||||
## - Check if a given array (parameter 2) contains a given string (parameter 1)
|
||||
## -
|
||||
containsElement () {
|
||||
local e
|
||||
for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
## - Percentage (1 decimal) of parameter 1 (part) against parameter 2 (total)
|
||||
## - Returns "0.0" if total is empty, zero or non-numeric.
|
||||
## -
|
||||
calc_percent() {
|
||||
local _part="$1"
|
||||
local _total="$2"
|
||||
if [[ -z "$_total" ]] || ! [[ "$_total" =~ ^[0-9]+$ ]] || [[ "$_total" -eq 0 ]] ; then
|
||||
echo "0.0"
|
||||
else
|
||||
awk -v b="$_part" -v t="$_total" 'BEGIN { printf "%.1f", (b/t)*100 }'
|
||||
fi
|
||||
}
|
||||
|
||||
blank_line() {
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
# - Running in a terminal?
|
||||
# -
|
||||
if [[ -t 1 ]] ; then
|
||||
terminal=true
|
||||
else
|
||||
terminal=false
|
||||
fi
|
||||
|
||||
# ----------
|
||||
# - Jobhandling
|
||||
# ----------
|
||||
|
||||
if pgrep -f "$(basename $0)" | grep -q -v $$ ; then
|
||||
|
||||
msg="A previos instance of script \"`basename $0`\" seems already be running."
|
||||
|
||||
echo ""
|
||||
if $terminal ; then
|
||||
echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg"
|
||||
echo ""
|
||||
echo -e " \033[31m\033[1mScript was interupted\033[m!"
|
||||
else
|
||||
echo " [ Fatal ]: $msg"
|
||||
echo ""
|
||||
echo " Script was interupted!"
|
||||
fi
|
||||
echo
|
||||
|
||||
exit 1
|
||||
else
|
||||
if [[ -d "$LOCK_DIR" ]] ; then
|
||||
rm -rf "$LOCK_DIR" 2> /dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# - If job already runs, stop execution..
|
||||
# -
|
||||
if mkdir "$LOCK_DIR" 2> /dev/null ; then
|
||||
|
||||
# - Remove lockdir when the script finishes, or when it receives a signal
|
||||
# -
|
||||
trap clean_up SIGHUP SIGINT SIGTERM
|
||||
|
||||
else
|
||||
|
||||
msg="A previos instance of script \"`basename $0`\" seems already be running."
|
||||
|
||||
echo ""
|
||||
if $terminal ; then
|
||||
echo -e "[ \033[31m\033[1mFatal\033[m ]: $msg"
|
||||
echo ""
|
||||
echo -e " \033[31m\033[1mScript was interupted\033[m!"
|
||||
else
|
||||
echo " [ Fatal ]: $msg"
|
||||
echo ""
|
||||
echo " Script was interupted!"
|
||||
fi
|
||||
echo
|
||||
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
|
||||
# -------------
|
||||
# - Read in Commandline arguments
|
||||
# -------------
|
||||
while getopts hs: opt ; do
|
||||
case $opt in
|
||||
h) usage ;;
|
||||
s) WEBSITE=$OPTARG ;;
|
||||
\?) usage
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$WEBSITE" ]] ; then
|
||||
|
||||
while IFS='' read -r -d '' _conf_file ; do
|
||||
source $_conf_file
|
||||
if [[ -n "$WEBSITE" ]] ; then
|
||||
unsorted_website_arr+=("${WEBSITE}:$_conf_file")
|
||||
fi
|
||||
WEBSITE=""
|
||||
done < <(find "${conf_dir}" -maxdepth 1 -type f -name "*.conf" -print0)
|
||||
|
||||
# - Sort array
|
||||
# -
|
||||
IFS=$'\n' website_arr=($(sort <<<"${unsorted_website_arr[*]}"))
|
||||
|
||||
# Which cloud instance (website) would you like to update
|
||||
#
|
||||
source ${snippet_dir}/get-cloud-instance-to-update.sh
|
||||
|
||||
else
|
||||
|
||||
while IFS='' read -r -d '' _conf_file ; do
|
||||
if $(grep -E -q "WEBSITE=\"?${WEBSITE}\"?" ${_conf_file} 2> /dev/null) ; then
|
||||
conf_file="${_conf_file}"
|
||||
break
|
||||
fi
|
||||
done < <(find "${conf_dir}" -maxdepth 1 -type f -name "*.conf" -print0)
|
||||
|
||||
fi
|
||||
|
||||
|
||||
# - Reset IFS
|
||||
# -
|
||||
IFS=$CUR_IFS
|
||||
|
||||
DEFAULT_SRC_BASE_DIR="/usr/local/src/nextcloud"
|
||||
DEFAULT_HTTP_USER="www-data"
|
||||
DEFAULT_HTTP_GROUP="www-data"
|
||||
DEFAULT_PHP_ENGINE='FPM'
|
||||
|
||||
blank_line
|
||||
echononl " Include Configuration file '$(basename "${conf_file}")'.."
|
||||
if [[ ! -f $conf_file ]]; then
|
||||
echo_skipped
|
||||
fatal "Missing configuration file '$conf_file'."
|
||||
else
|
||||
source $conf_file
|
||||
echo_ok
|
||||
fi
|
||||
|
||||
DEFAULT_WEB_BASE_DIR="/var/www/${WEBSITE}"
|
||||
[[ -n "$WEB_BASE_DIR" ]] || WEB_BASE_DIR=$DEFAULT_WEB_BASE_DIR
|
||||
|
||||
if [[ ! -d ${WEB_BASE_DIR} ]] ; then
|
||||
fatal "Web base directory '$WEB_BASE_DIR' not found!"
|
||||
fi
|
||||
|
||||
DATA_DIR="$(realpath ${WEB_BASE_DIR}/data)"
|
||||
|
||||
[[ -n "$PHP_ENGINE" ]] || PHP_ENGINE=$DEFAULT_PHP_ENGINE
|
||||
|
||||
INSTALL_DIR="$(realpath ${WEB_BASE_DIR}/nextcloud)"
|
||||
CURRENT_VERSION="$(basename $INSTALL_DIR | cut -d"-" -f2)"
|
||||
|
||||
|
||||
# =============
|
||||
# --- Some
|
||||
# =============
|
||||
|
||||
# - Support systemd ?
|
||||
# -
|
||||
SYSTEMD_EXISTS=false
|
||||
systemd=$(which systemd)
|
||||
systemctl=$(which systemctl)
|
||||
|
||||
if [[ -n "$systemd" ]] || [[ -n "$systemctl" ]] ; then
|
||||
SYSTEMD_EXISTS=true
|
||||
fi
|
||||
|
||||
#clear
|
||||
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
echo -e "\033[32m-----\033[m"
|
||||
echo -e "Scan encrypted files for \033[1mBad Signature\033[m errors on system \033[1m${WEB_BASE_DIR}\033[m"
|
||||
echo -e "\033[1m
|
||||
Read-only: every file is decrypted once and the content discarded,
|
||||
reporting every file that fails Server-Side Encryption's signature
|
||||
check. Nothing is changed, moved or deleted.\033[m"
|
||||
echo -e "\033[32m-----\033[m"
|
||||
fi
|
||||
|
||||
|
||||
# =============
|
||||
# --- Some checks
|
||||
# =============
|
||||
|
||||
DEFAULT_HTTP_USER="www-data"
|
||||
DEFAULT_HTTP_GROUP="www-data"
|
||||
|
||||
|
||||
NGINX_IS_ENABLED=false
|
||||
APACHE2_IS_ENABLED=false
|
||||
|
||||
# Get Webservice environment as IS_HTTPD_RUNNING, HTTP_USER, HTTP_GROUP..
|
||||
#
|
||||
source ${snippet_dir}/get-webservice-environment.sh
|
||||
|
||||
|
||||
# Check PHP Version
|
||||
#
|
||||
source ${snippet_dir}/get-php-major-version.sh
|
||||
|
||||
|
||||
# Get full qualified PHP command
|
||||
#
|
||||
source ${snippet_dir}/get-path-of-php-command.sh
|
||||
|
||||
|
||||
if [[ ! -x "$PHP_BIN" ]]; then
|
||||
fatal "No PHP binary found!"
|
||||
fi
|
||||
|
||||
|
||||
# =============
|
||||
# --- Determine existing accounts
|
||||
# =============
|
||||
|
||||
blank_line
|
||||
echononl " Get list of current accounts.."
|
||||
|
||||
mapfile -t unsorted_account_arr < <(su -c "${PHP_BIN} ${INSTALL_DIR}/occ user:list" -s /bin/bash $HTTP_USER 2> ${log_file} | awk -F'[:[:space:]]+' '{print $3}' 2>> ${log_file} )
|
||||
|
||||
if [[ $? -gt 0 ]] || [[ -s "${log_file}" ]] ; then
|
||||
echo_failed
|
||||
fatal "$(cat $log_file)"
|
||||
else
|
||||
echo_ok
|
||||
> "$log_file"
|
||||
fi
|
||||
|
||||
# - Sort array
|
||||
# -
|
||||
IFS=$'\n' account_arr=($(sort <<<"${unsorted_account_arr[*]}"))
|
||||
IFS=$CUR_IFS
|
||||
|
||||
|
||||
# =============
|
||||
# --- Ask which account(s) to scan
|
||||
# =============
|
||||
|
||||
echo ""
|
||||
echo ""
|
||||
echo -e "\033[32m-----\033[m"
|
||||
echo ""
|
||||
echo -e " Existing accounts on \033[1m${WEBSITE}\033[m\n"
|
||||
echo -en "\033[33m"
|
||||
printf " %s\n" "${account_arr[@]}"
|
||||
echo -en "\033[m"
|
||||
blank_line
|
||||
|
||||
echo ""
|
||||
echo ""
|
||||
echo -e "\033[32m-----\033[m"
|
||||
echo ""
|
||||
echo -e " Which account(s) would you like to scan for 'Bad Signature' errors?"
|
||||
echo ""
|
||||
echo " - enter a list separated by spaces"
|
||||
echo -e " - or type \033[33mall\033[m to scan every existing account"
|
||||
echo ""
|
||||
SELECTED_USERS=
|
||||
while [[ -z "$(trim ${SELECTED_USERS})" ]] ; do
|
||||
|
||||
echononl " Account(s) to scan: "
|
||||
read SELECTED_USERS
|
||||
|
||||
if [[ -z "$(trim ${SELECTED_USERS})" ]] ; then
|
||||
echo -e "\n\t\033[33m\033[1mEntry must not be empty. Type \033[m\033[1mall\033[33m to scan every account.\033[m\n"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "${SELECTED_USERS,,}" = "all" ]] ; then
|
||||
selected_user_arr=("${account_arr[@]}")
|
||||
break
|
||||
fi
|
||||
|
||||
IFS=' ' read -ra unsorted_selected_user_arr <<< "${SELECTED_USERS}"
|
||||
|
||||
_all_valid=true
|
||||
for _user in "${unsorted_selected_user_arr[@]}" ; do
|
||||
if ! containsElement "${_user}" "${account_arr[@]}" ; then
|
||||
echo -e "\n\t\033[33m\033[1m No account \033[m\033[1m${_user}\033[33m present!\033[m - Try again..\n"
|
||||
SELECTED_USERS=""
|
||||
_all_valid=false
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
$_all_valid || continue
|
||||
|
||||
# - Sort array
|
||||
# -
|
||||
IFS=$'\n' selected_user_arr=($(sort <<<"${unsorted_selected_user_arr[*]}"))
|
||||
IFS=$CUR_IFS
|
||||
|
||||
done
|
||||
|
||||
|
||||
# =============
|
||||
# --- Write out the (embedded) PHP scan script into the Nextcloud
|
||||
# --- install directory - it needs to sit next to 'occ' since it
|
||||
# --- bootstraps Nextcloud the same way occ does.
|
||||
# =============
|
||||
|
||||
scan_php_file="${INSTALL_DIR}/.occ_scan_bad_signature_$$.php"
|
||||
|
||||
cat > "$scan_php_file" <<'PHP_SCAN_EOF'
|
||||
<?php
|
||||
/**
|
||||
* .occ_scan_bad_signature_*.php
|
||||
*
|
||||
* READ-ONLY Diagnose-Skript fuer Nextcloud Server-Side Encryption.
|
||||
* Wird von occ_scan_bad_signature.sh automatisch erzeugt und nach
|
||||
* Gebrauch wieder geloescht - nicht von Hand aufrufen/aendern.
|
||||
*
|
||||
* Was es tut:
|
||||
* Geht durch alle Dateien EINES uebergebenen Benutzers und liest
|
||||
* jede Datei einmal vollstaendig (im Stream, ohne den Inhalt zu
|
||||
* speichern). Dadurch wird fuer jede verschluesselte Datei die
|
||||
* vollstaendige Entschluesselung inkl. Signaturpruefung jedes
|
||||
* Blocks erzwungen - genau der Schritt, der beim "Bad Signature"
|
||||
* Fehler fehlschlaegt.
|
||||
*
|
||||
* Was es NICHT tut:
|
||||
* Es schreibt, aendert oder loescht NICHTS. Reiner Lesezugriff.
|
||||
*
|
||||
* Aufruf:
|
||||
* php .occ_scan_bad_signature_*.php <benutzername> [--before=YYYY-MM-DD] [--after=YYYY-MM-DD]
|
||||
*
|
||||
* Ausgabe (TSV, nach STDOUT):
|
||||
* user \t path \t size_bytes \t mtime \t error
|
||||
* Eine Zeile pro Datei, bei der das Lesen fehlgeschlagen ist.
|
||||
* Fortschritts-/Debug-/Zusammenfassungsmeldungen gehen nach STDERR.
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
fwrite(STDERR, "DEBUG: Skript gestartet, PID=" . getmypid() . "\n");
|
||||
|
||||
// Faengt fatale, NICHT als Throwable auftretende Fehler ab (z.B. Memory
|
||||
// Limit exhausted), die sonst komplett stumm bleiben wuerden.
|
||||
register_shutdown_function(function () {
|
||||
$err = error_get_last();
|
||||
if ($err !== null && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
|
||||
fwrite(STDERR, "DEBUG: FATAL bei Shutdown: [{$err['type']}] {$err['message']} in {$err['file']}:{$err['line']}\n");
|
||||
} else {
|
||||
fwrite(STDERR, "DEBUG: Shutdown ohne erkannten Fatal-Error.\n");
|
||||
}
|
||||
});
|
||||
|
||||
// WICHTIG: Muss VOR dem require von lib/base.php gesetzt werden.
|
||||
// Ohne diese Konstante fuehrt lib/base.php am Ende automatisch
|
||||
// OC::handleRequest() aus (als waere dies ein normaler HTTP-Request).
|
||||
define('OC_CONSOLE', 1);
|
||||
|
||||
$oldWorkingDir = getcwd();
|
||||
if ($oldWorkingDir === false) {
|
||||
fwrite(STDERR, "Konnte aktuelles Arbeitsverzeichnis nicht ermitteln. Bitte mit absolutem Pfad aufrufen.\n");
|
||||
exit(1);
|
||||
}
|
||||
chdir(__DIR__);
|
||||
|
||||
fwrite(STDERR, "DEBUG: vor require lib/base.php\n");
|
||||
require_once __DIR__ . '/lib/base.php';
|
||||
fwrite(STDERR, "DEBUG: nach require lib/base.php - Bootstrap abgeschlossen\n");
|
||||
|
||||
chdir($oldWorkingDir);
|
||||
|
||||
if (function_exists('posix_getuid') && posix_getuid() === 0) {
|
||||
fwrite(STDERR, "Bitte NICHT als root ausfuehren, sondern als Webserver-User, z.B.:\n");
|
||||
fwrite(STDERR, " sudo -u www-data php " . __FILE__ . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Ab hier alles in einem Catch-All, damit ein Throwable garantiert HIER
|
||||
// landet und nicht beim (auf STDOUT/STDERR ggf. still bleibenden)
|
||||
// globalen Nextcloud-Handler.
|
||||
try {
|
||||
fwrite(STDERR, "DEBUG: hole UserManager/RootFolder ...\n");
|
||||
|
||||
// Argumente einlesen: erstes nicht-Options-Argument = Benutzername
|
||||
// (optional, sonst alle Benutzer). --before=YYYY-MM-DD prueft nur
|
||||
// Dateien, die VOR diesem Datum zuletzt geaendert wurden - spart bei
|
||||
// grossen Accounts viel Zeit, sobald der Cutoff-Zeitpunkt bekannt ist.
|
||||
$onlyUser = null;
|
||||
$beforeTs = null;
|
||||
$afterTs = null;
|
||||
foreach (array_slice($argv, 1) as $arg) {
|
||||
if (str_starts_with($arg, '--before=')) {
|
||||
$beforeTs = strtotime(substr($arg, 9));
|
||||
} elseif (str_starts_with($arg, '--after=')) {
|
||||
$afterTs = strtotime(substr($arg, 8));
|
||||
} elseif ($onlyUser === null) {
|
||||
$onlyUser = $arg;
|
||||
}
|
||||
}
|
||||
if ($beforeTs !== null) {
|
||||
fwrite(STDERR, "DEBUG: Filter --before " . date('Y-m-d', $beforeTs) . "\n");
|
||||
}
|
||||
if ($afterTs !== null) {
|
||||
fwrite(STDERR, "DEBUG: Filter --after " . date('Y-m-d', $afterTs) . "\n");
|
||||
}
|
||||
|
||||
// Neuere Nextcloud-Versionen haben die alten Komfort-Methoden wie
|
||||
// getUserManager()/getRootFolder() auf \OC::$server entfernt - der
|
||||
// unterstuetzte Weg ist jetzt der PSR-11-Container ->get(...).
|
||||
$userManager = \OC::$server->get(\OCP\IUserManager::class);
|
||||
$rootFolder = \OC::$server->get(\OCP\Files\IRootFolder::class);
|
||||
fwrite(STDERR, "DEBUG: UserManager/RootFolder OK\n");
|
||||
|
||||
$total = 0;
|
||||
$skipped = 0;
|
||||
$bad = 0;
|
||||
$usersScanned = 0;
|
||||
|
||||
echo "user\tpath\tsize_bytes\tmtime\terror\n";
|
||||
flush();
|
||||
|
||||
/**
|
||||
* Rekursiv durch einen Ordner gehen und jede Datei vollstaendig lesen
|
||||
* (ausser sie liegt ausserhalb von --before/--after).
|
||||
*/
|
||||
$scanFolder = function (\OCP\Files\Folder $folder, string $uid) use (&$scanFolder, &$total, &$skipped, &$bad, $beforeTs, $afterTs) {
|
||||
foreach ($folder->getDirectoryListing() as $node) {
|
||||
if ($node instanceof \OCP\Files\Folder) {
|
||||
$scanFolder($node, $uid);
|
||||
continue;
|
||||
}
|
||||
if (!($node instanceof \OCP\Files\File)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mtimeRaw = $node->getMTime();
|
||||
if (($beforeTs !== null && $mtimeRaw >= $beforeTs) || ($afterTs !== null && $mtimeRaw <= $afterTs)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$total++;
|
||||
$path = $node->getPath();
|
||||
|
||||
try {
|
||||
$stream = $node->fopen('r');
|
||||
if ($stream === false) {
|
||||
throw new \RuntimeException('fopen lieferte false');
|
||||
}
|
||||
while (!feof($stream)) {
|
||||
$chunk = fread($stream, 8 * 1024 * 1024);
|
||||
if ($chunk === false) {
|
||||
throw new \RuntimeException('fread schlug mitten im Stream fehl');
|
||||
}
|
||||
}
|
||||
fclose($stream);
|
||||
} catch (\Throwable $e) {
|
||||
$bad++;
|
||||
$mtime = date('c', $node->getMTime());
|
||||
$msg = str_replace(["\t", "\n", "\r"], ' ', $e->getMessage());
|
||||
echo "$uid\t$path\t{$node->getSize()}\t$mtime\t" . get_class($e) . ": $msg\n";
|
||||
flush();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scanUser = function (\OCP\IUser $user) use (&$scanFolder, $rootFolder, &$usersScanned) {
|
||||
$uid = $user->getUID();
|
||||
fwrite(STDERR, "Scanne Benutzer: $uid ...\n");
|
||||
|
||||
\OC_Util::setupFS($uid);
|
||||
|
||||
try {
|
||||
$userFolder = $rootFolder->getUserFolder($uid);
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, " Ueberspringe $uid: kann User-Folder nicht laden (" . $e->getMessage() . ")\n");
|
||||
return;
|
||||
}
|
||||
|
||||
$scanFolder($userFolder, $uid);
|
||||
$usersScanned++;
|
||||
};
|
||||
|
||||
if ($onlyUser !== null) {
|
||||
fwrite(STDERR, "DEBUG: suche Benutzer '$onlyUser' ...\n");
|
||||
$user = $userManager->get($onlyUser);
|
||||
if ($user === null) {
|
||||
fwrite(STDERR, "Benutzer '$onlyUser' nicht gefunden.\n");
|
||||
exit(1);
|
||||
}
|
||||
fwrite(STDERR, "DEBUG: Benutzer gefunden, starte Scan ...\n");
|
||||
$scanUser($user);
|
||||
} else {
|
||||
$userManager->callForAllUsers($scanUser);
|
||||
}
|
||||
|
||||
fwrite(STDERR, "\nFertig. Benutzer gescannt: $usersScanned, Dateien geprueft: $total, uebersprungen (Filter): $skipped, fehlgeschlagen: $bad\n");
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, "\n!!! UNBEHANDELTE AUSNAHME !!!\n");
|
||||
fwrite(STDERR, get_class($e) . ": " . $e->getMessage() . "\n");
|
||||
fwrite(STDERR, "in " . $e->getFile() . ":" . $e->getLine() . "\n");
|
||||
fwrite(STDERR, $e->getTraceAsString() . "\n");
|
||||
exit(2);
|
||||
}
|
||||
PHP_SCAN_EOF
|
||||
|
||||
chmod 644 "$scan_php_file" 2> /dev/null
|
||||
|
||||
mkdir -p "$report_dir" 2> /dev/null
|
||||
report_file="${report_dir}/bad_signature_${WEBSITE}_${run_date}.tsv"
|
||||
|
||||
{
|
||||
echo "=================================================================="
|
||||
echo " Bad-Signature-Scan ${WEBSITE} ${run_date}"
|
||||
echo "=================================================================="
|
||||
echo ""
|
||||
echo -e "user\tpath\tsize_bytes\tmtime\terror"
|
||||
} > "$report_file"
|
||||
|
||||
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
echo -e "\033[1;32mStarting Script for \033[1;37m${WEBSITE}\033[m"
|
||||
echo ""
|
||||
echo -e " Cloud instance to be scanned.........: $WEBSITE"
|
||||
echo ""
|
||||
echo -e " Current version of nextcloud.........: $CURRENT_VERSION"
|
||||
echo ""
|
||||
echo -e " Web base directory...................: $WEB_BASE_DIR"
|
||||
echo -e " Install directory....................: $INSTALL_DIR"
|
||||
echo -e " Data directory.......................: $DATA_DIR"
|
||||
echo ""
|
||||
echo -e " Webserver user.......................: $HTTP_USER"
|
||||
echo -e " Webserver group......................: $HTTP_GROUP"
|
||||
echo ""
|
||||
echo -e " PHP command..........................: $PHP_BIN"
|
||||
echo ""
|
||||
echo -e " Account(s) to scan...................: \033[33m${selected_user_arr[*]}\033[m"
|
||||
echo ""
|
||||
echo -e " Report file...........................: $report_file"
|
||||
echo ""
|
||||
|
||||
echo ""
|
||||
echo -n " Type upper case 'YES' to continue executing with this parameters: "
|
||||
read OK
|
||||
if [[ "$OK" = "YES" ]] ; then
|
||||
echo ""
|
||||
echo ""
|
||||
echo -e "\033[1;32mGoing to scan encrypted files for each selected account on \033[1;37m$WEBSITE \033[m"
|
||||
else
|
||||
fatal "Abort by user request - Answer as not 'YES'"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# -----
|
||||
# - Main part of the script
|
||||
# -----
|
||||
|
||||
if $terminal ; then
|
||||
echo ""
|
||||
echo ""
|
||||
echo -e "\033[37m\033[1mMain part of the script\033[m"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
declare -i total_checked=0
|
||||
declare -i total_bad=0
|
||||
declare -i total_failed_users=0
|
||||
|
||||
for _user in "${selected_user_arr[@]}" ; do
|
||||
|
||||
user_tsv="${LOCK_DIR}/scan_${_user}.tsv"
|
||||
|
||||
# - Gut sichtbarer Account-Abschnitt im Reportfile - wird IMMER
|
||||
# - angelegt, auch wenn der Scan fuer diesen Account fehlschlaegt.
|
||||
# -
|
||||
_sep_len=${#_user}
|
||||
[[ $_sep_len -lt 9 ]] && _sep_len=9
|
||||
_sep_line="$(printf '%*s' "$_sep_len" '' | tr ' ' '-')"
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo ""
|
||||
echo "$_sep_line"
|
||||
echo "$_user"
|
||||
echo "$_sep_line"
|
||||
} >> "$report_file"
|
||||
|
||||
echononl " Scanning account \033[1;37m${_user}\033[m.."
|
||||
su -c "$PHP_BIN $scan_php_file $_user" -s /bin/bash $HTTP_USER > "$user_tsv" 2> "$log_file"
|
||||
_rc=$?
|
||||
|
||||
if [[ $_rc -ne 0 ]]; then
|
||||
echo_failed
|
||||
error "$(cat "$log_file")"
|
||||
echo " [ FEHLER beim Scan - siehe Server-Log ]" >> "$report_file"
|
||||
(( total_failed_users++ ))
|
||||
continue
|
||||
fi
|
||||
|
||||
# - Kopfzeile weglassen und im Account-Abschnitt anhaengen
|
||||
# -
|
||||
tail -n +2 "$user_tsv" >> "$report_file"
|
||||
|
||||
_user_bad=$(tail -n +2 "$user_tsv" | wc -l)
|
||||
(( total_bad += _user_bad ))
|
||||
|
||||
# - Scan lief durch: bei Funden gelb hervorheben statt schlichtem 'ok'
|
||||
# -
|
||||
if [[ $_user_bad -gt 0 ]] ; then
|
||||
echo_funde
|
||||
else
|
||||
echo_ok
|
||||
fi
|
||||
|
||||
# - Leerzeile NACH der 'Scanning account ..'-Zeile, vor der
|
||||
# - Account-Statistik.
|
||||
# -
|
||||
$terminal && echo ""
|
||||
|
||||
# - Kennzahlen aus der STDERR-Zusammenfassung herausziehen
|
||||
# -
|
||||
_summary="$(grep -E '^Fertig\.' "$log_file")"
|
||||
_checked="$(echo "$_summary" | grep -oE 'Dateien geprueft: [0-9]+' | grep -oE '[0-9]+')"
|
||||
_skipped="$(echo "$_summary" | grep -oE 'uebersprungen \(Filter\): [0-9]+' | grep -oE '[0-9]+')"
|
||||
[[ -z "$_skipped" ]] && _skipped="0"
|
||||
|
||||
if [[ "$_checked" =~ ^[0-9]+$ ]] ; then
|
||||
(( total_checked += _checked ))
|
||||
_pct="$(calc_percent "$_user_bad" "$_checked")"
|
||||
else
|
||||
_checked="?"
|
||||
_pct="?"
|
||||
fi
|
||||
|
||||
# - Ergebnis-Block pro Account - gleiches Looking wie das
|
||||
# - Gesamtergebnis am Ende (Konsole + Reportfile)
|
||||
# -
|
||||
if $terminal ; then
|
||||
echo -e " \033[1;37mAccount ${_user}\033[m"
|
||||
echo -e " Gescannte Dateien.......................: ${_checked}"
|
||||
echo -e " Uebersprungene Dateien...................: ${_skipped}"
|
||||
if [[ $_user_bad -gt 0 ]] ; then
|
||||
echo -e " Dateien mit 'Bad Signature'..............: \033[1;33m${_user_bad} (${_pct} %)\033[m"
|
||||
else
|
||||
echo -e " Dateien mit 'Bad Signature'..............: ${_user_bad} (${_pct} %)"
|
||||
fi
|
||||
# - Leerzeile HINTER der Account-Statistik, damit sie nicht am
|
||||
# - 'Scanning account ..' des naechsten Accounts klebt.
|
||||
# -
|
||||
echo ""
|
||||
fi
|
||||
|
||||
{
|
||||
echo " Account ${_user}"
|
||||
echo " Gescannte Dateien.......................: ${_checked}"
|
||||
echo " Uebersprungene Dateien...................: ${_skipped}"
|
||||
echo " Dateien mit 'Bad Signature'..............: ${_user_bad} (${_pct} %)"
|
||||
} >> "$report_file"
|
||||
|
||||
done
|
||||
|
||||
total_pct="$(calc_percent "$total_bad" "$total_checked")"
|
||||
|
||||
# - Gesamtergebnis ans Ende des Reportfiles - gleiches Looking wie
|
||||
# - die Ergebnis-Bloecke pro Account
|
||||
# -
|
||||
{
|
||||
echo ""
|
||||
echo ""
|
||||
echo "=================================================================="
|
||||
echo " Gesamtergebnis"
|
||||
echo "=================================================================="
|
||||
echo ""
|
||||
echo "Gescannte Accounts.........................: ${#selected_user_arr[@]}"
|
||||
[[ $total_failed_users -gt 0 ]] && echo "Accounts mit Scan-Fehler...................: ${total_failed_users}"
|
||||
echo "Gescannte Dateien insgesamt.................: ${total_checked}"
|
||||
echo "Dateien mit 'Bad Signature' insgesamt.......: ${total_bad} (${total_pct} %)"
|
||||
} >> "$report_file"
|
||||
|
||||
blank_line
|
||||
|
||||
if $terminal ; then
|
||||
echo -e "\033[37m\033[1mErgebnis\033[m"
|
||||
echo ""
|
||||
echo -e " Gescannte Accounts.........................: ${#selected_user_arr[@]}"
|
||||
[[ $total_failed_users -gt 0 ]] && echo -e " Accounts mit Scan-Fehler...................: \033[1;31m${total_failed_users}\033[m"
|
||||
echo -e " Gescannte Dateien insgesamt.................: ${total_checked}"
|
||||
echo -e " Dateien mit 'Bad Signature' insgesamt.......: \033[1;31m${total_bad} (${total_pct} %)\033[m"
|
||||
echo -e " Report-Datei.................................: ${report_file}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
clean_up 0
|
||||
+13
-13
@@ -813,14 +813,8 @@ if ! $_revalidate_only_explicit && $terminal ; then
|
||||
blank_line
|
||||
echo -e "\033[37m\033[1mWhich mode should this run use?\033[m"
|
||||
echo ""
|
||||
echo -e " \033[1m[1] Recovery\033[m - decrypt bad-signature files (temporarily skips the
|
||||
signature check), write them under
|
||||
${DEFAULT_RECOVERY_BASE_DIR}/<website>
|
||||
and validate them"
|
||||
echo ""
|
||||
echo " [2] Revalidate-only - re-run just the validation checks against files a previous
|
||||
recovery run already wrote to disk (same as '-V'); nothing
|
||||
is decrypted again and no config value is touched"
|
||||
echo -e " \033[1m[1] Recovery\033[m - decrypt bad-signature files (temporarily skips the signature check), write them under ${DEFAULT_RECOVERY_BASE_DIR}/<website> and validate them"
|
||||
echo " [2] Revalidate-only - re-run just the validation checks against files a previous recovery run already wrote to disk (same as '-V'); nothing is decrypted again and no config value is touched"
|
||||
info "Just press Return to use the default: [1] Recovery."
|
||||
echo -n " Select mode by number [1]: "
|
||||
read _mode_choice
|
||||
@@ -1425,9 +1419,9 @@ for _user in "${selected_user_arr[@]}" ; do
|
||||
_local="${user_out_dir}/${_rel}"
|
||||
if [[ -f "$_local" ]] ; then
|
||||
_b=$(stat -c%s "$_local" 2> /dev/null)
|
||||
echo -e "${_p}\t${_b:-0}\t${_osize}\tOK"
|
||||
printf '%s\t%s\t%s\t%s\n' "$_p" "${_b:-0}" "$_osize" "OK"
|
||||
else
|
||||
echo -e "${_p}\t0\t${_osize}\tNOT_RECOVERED: no file found under ${user_out_dir} (run without -V first)"
|
||||
printf '%s\t%s\t%s\t%s\n' "$_p" "0" "$_osize" "NOT_RECOVERED: no file found under ${user_out_dir} (run without -V first)"
|
||||
fi
|
||||
done < "$list_file"
|
||||
} > "$user_result_tsv"
|
||||
@@ -1484,7 +1478,13 @@ for _user in "${selected_user_arr[@]}" ; do
|
||||
(( _user_read_errors++ ))
|
||||
fi
|
||||
|
||||
echo -e "${_path}\t${_recbytes}\t${_origsize}\t${_status}\t${_val_class}\t${_val_detail}" >> "$recovery_report_file"
|
||||
# printf, not 'echo -e': $_val_detail (or $_status for a
|
||||
# READ_ERROR row) can contain a raw PHP exception message (e.g.
|
||||
# 'OCA\Encryption\Exceptions\...') - 'echo -e' would reinterpret
|
||||
# those backslashes as escape sequences and silently mangle/eat
|
||||
# parts of the text. printf's %s never reinterprets its argument,
|
||||
# only the literal \t in the format string itself.
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$_path" "$_recbytes" "$_origsize" "$_status" "$_val_class" "$_val_detail" >> "$recovery_report_file"
|
||||
|
||||
done < "$user_result_tsv"
|
||||
|
||||
@@ -1522,7 +1522,7 @@ for _user in "${selected_user_arr[@]}" ; do
|
||||
echo " Datenmuell (ungueltig) - Account ${_user} (${_user_invalid})"
|
||||
echo " ------------------------------------------------------------------"
|
||||
for _line in "${_user_invalid_lines[@]}" ; do
|
||||
echo -e " ${_line}"
|
||||
printf ' %s\n' "$_line"
|
||||
done
|
||||
fi
|
||||
if [[ $_user_unverified -gt 0 ]] ; then
|
||||
@@ -1531,7 +1531,7 @@ for _user in "${selected_user_arr[@]}" ; do
|
||||
echo " Nicht pruefbar - Account ${_user} (${_user_unverified})"
|
||||
echo " ------------------------------------------------------------------"
|
||||
for _line in "${_user_unverified_lines[@]}" ; do
|
||||
echo -e " ${_line}"
|
||||
printf ' %s\n' "$_line"
|
||||
done
|
||||
fi
|
||||
} >> "$recovery_report_file"
|
||||
|
||||
Executable
+2001
File diff suppressed because it is too large
Load Diff
Executable
+1620
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user