Remove -g option as it was broken and would never work. Open and read from file descriptor for input rather than keep accessing a file. Re-worked code around the user changing facility - it now actually works \o/. Minor tweeks to the help text.
749 lines
26 KiB
Bash
Executable file
749 lines
26 KiB
Bash
Executable file
#!/bin/bash
|
|
# Copyright (c) 2018-2020:
|
|
# Darren 'Tadgy' Austin <darren (at) afterdark.org.uk>
|
|
# Licensed under the terms of the GNU General Public License version 3.
|
|
#
|
|
#........1.........2.........3.........4.........5.........6.........7.........8.........9.........0.........1..:......2.........3.........4.........5.........:
|
|
|
|
|
|
# Script details.
|
|
NAME="${0##*/}"
|
|
VERSION="0.2.2"
|
|
|
|
|
|
# Functions.
|
|
check_leading_dirs() {
|
|
# $1 The virtual host being processed.
|
|
# $2 The path of which to validate the leading directories.
|
|
|
|
[[ -z "$1" || -z "$2" ]] && return 1
|
|
|
|
if ! is_dir "$(remove_expansions "$2")"; then
|
|
(( FLAGS[${1}_template-prefix] == 0 )) && {
|
|
syslog "warn" "prefix directories of template do not exist: $(remove_expansions "$2")"
|
|
FLAGS[${1}_template-prefix]=1
|
|
}
|
|
return 1
|
|
else
|
|
(( FLAGS[${1}_template-prefix] == 1 )) && {
|
|
syslog "info" "prefix directories of template reappeared: $(remove_expansions "$2")"
|
|
FLAGS[${1}_template-prefix]=0
|
|
}
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
close_fd() {
|
|
# $1 The site of which to close the file descriptor.
|
|
|
|
[[ -z "$1" ]] && return 1
|
|
|
|
# shellcheck disable=SC1083
|
|
{ exec {FDS[$1]}>&-; } 2>/dev/null || syslog "warn" "failed to close FD ${FDS[$1]} for $1"
|
|
unset "FDS[$1]" "FLAGS[${1}_template-prefix]" "FLAGS[${1}_make-dir-fail]" "FLAGS[${1}_fix-link]"
|
|
}
|
|
|
|
create_missing_dirs() {
|
|
# $1 The virtual host being processed.
|
|
# $2 The directory to insure exists.
|
|
|
|
[[ -z "$1" || -z "$2" ]] && return 1
|
|
|
|
if ! make_dir "$2"; then
|
|
(( FLAGS[${1}_make-dir-fail] == 0 )) && {
|
|
syslog "warn" "error creating log file's directory: $2"
|
|
FLAGS[${1}_make-dir-fail]=1
|
|
}
|
|
return 1
|
|
else
|
|
(( FLAGS[${1}_make-dir-fail] == 1 )) && {
|
|
syslog "info" "created log file's directory: $2"
|
|
FLAGS[${1}_make-dir-fail]=0
|
|
}
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
die() {
|
|
# $1 The text of the error message to display on stderr.
|
|
|
|
printf "%s: %s\\n" "$NAME" "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
display_help() {
|
|
# |........1.........2.........3.........4.........5.........6.........7.........8
|
|
cat <<-EOF
|
|
Usage: $NAME [options] <basedir> <template>
|
|
Process input (possibly including an httpd VirtualHost site identifier) from
|
|
stdin or a pipe/FIFO and write a log line to a log file based upon the <basedir>
|
|
and <template>.
|
|
|
|
Options (all of which are optional):
|
|
-ca <arg> Set the compression command arguments. Default: ${COMPRESSOR_ARGS[@]}.
|
|
Quotes are required if more than one <arg> is supplied.
|
|
-cc <util> Set the compression command to use. Default: $COMPRESSOR.
|
|
-f Request flushing of the log file to disk after every write.
|
|
This may significantly reduce performance and result in a lot of
|
|
disk writes. Best to let the kernel do appropriate buffering.
|
|
-h Display this help.
|
|
-i <pipe> Read input from the pipe/FIFO at <pipe>, rather than stdin.
|
|
If the pipe/FIFO does not exist, it will be created. Use '-o'
|
|
to set the ownership of the pipe/FIFO. The pipe/FIFO is created
|
|
before any user or group switching is performed.
|
|
-j <jobs> Maximum number of compression jobs to have active at once.
|
|
Default: $MAXJOBS. Don't set this too high.
|
|
-l <link> Create a symlink named <link> to the currently active log file.
|
|
The <link> is created relative to <basedir>. In normal mode,
|
|
the link name may include the same '{}' sequence and %-escaped
|
|
formatting as the <template> (see below). In raw mode (-r), the
|
|
'{}' is not allowed, but % escape sequences can still be used.
|
|
WARNING: The (expanded) location of this link will be WIPED OUT!
|
|
-md <umask> Set the umask used when creating directories. Default: $DIR_UMASK.
|
|
Useful umasks are: 077, 066, 026 and 022.
|
|
-mf <umask> Set the umask used when creating files. Default: $FILE_UMASK.
|
|
Useful umasks are: 066 and 022.
|
|
-mp <umask> Set the umask used when creating the pipe. Default: $PIPE_UMASK.
|
|
Useful umasks are: 066 and 006.
|
|
-o <owner> Set the owner of the pipe/FIFO automatically created if none
|
|
already exists, and '-i' is used. The <owner> should be in the
|
|
format [user]:[group], where [user] or [group] is optional, but
|
|
not both. The ownership is changed before any user or group
|
|
switching is performed. This option is only available to root.
|
|
-p Make all parents. Normally, all directories up to - but not
|
|
including - the first directory with non-escaped %-format
|
|
strings of the <template> (see below) must already exist for the
|
|
log lines to be written to the file. With this option, the
|
|
parent directories of the logfile will be created automatically.
|
|
WARNING: This option can be unsafe with certain <template>
|
|
formats - it can result in creation of arbitrary directories
|
|
based on unclean input from outside sources.
|
|
-r Raw logging mode. In this mode, no processing of the log line
|
|
for an httpd VirtualHost site identifier is performed - log
|
|
lines are written verbatim to the log filename constructed from
|
|
<basedir> and <template>.
|
|
-s <facility> Set the syslog facility to be used for logging. Default: $SYSLOG_FACILITY.
|
|
-u <user> Set name of the user to run with. With this option, as soon as
|
|
$NAME starts it will re-exec itself to run as this user.
|
|
Log files created by $NAME will be owned by this user and
|
|
its primary group. The default is to run as the user that
|
|
executed $NAME, which is usually root. This option is
|
|
only available to root.
|
|
-v Display version and copyright information.
|
|
-z Enable compression of the old log files.
|
|
-- Cease option processing and begin argument parsing.
|
|
Option processing ceases with the first non-option argument or --.
|
|
|
|
Arguments (all of which are mandatory):
|
|
<basedir> The base directory of where to write the log files. The base
|
|
directory must exist.
|
|
<template> The filename template. When in normal mode, the template must
|
|
include at least one occurrance of '{}', which is replaced with
|
|
the site name from the VirtualHost identifier. In raw mode
|
|
(-r), the '{}' should not be included in the template. The
|
|
template may also include any %-prefixed format strings
|
|
recognised by the strftime(3) function. See below for examples.
|
|
The template can not start with a / - it is relative to the
|
|
<basedir>. Unless the '-p' option is used, all directories up
|
|
to - but not including - the first directory with non-escaped
|
|
%-format strings of the <template> must already exist for the
|
|
log lines to be written to the file.
|
|
|
|
Examples:
|
|
When used with the httpd CustomLog directive, using %v as the first log format
|
|
string:
|
|
"|$NAME '/path/to/logsdir' '{}/logs/access-log-%Y-%m'"
|
|
Where the httpd VirtualHost identifier is 'example.com', would write logs
|
|
(with the site identifier stripped) to the filename:
|
|
/path/to/logsdir/example.com/logs/access-log-<year>-<month>
|
|
"|$NAME '/path/to/logsdir' '{}/logs/()-access-log-%Y-%m'"
|
|
Where the httpd VirtualHost identifier is 'example.com', would write logs
|
|
(with the site identifier steipped) to the filename:
|
|
/path/to/logsdir/example.com/logs/example.com-access-log-<year>-<month>
|
|
When used with the httpd ErrorLog directive (both examples are equilivent):
|
|
"|$NAME -r '/path/to/logsdir' 'logs/error-log-%Y-%m'"
|
|
Would write raw log lines to the filename:
|
|
/path/to/logsdir/logs/error-log-<year>-<month>
|
|
"|$NAME -r '/path/to/logsdir/logs' 'error-log-%Y-%m'"
|
|
Equilivant to the above; would write raw log lines to the filename:
|
|
/path/to/logsdir/logs/error-log-<year>-<month>
|
|
EOF
|
|
}
|
|
|
|
display_version() {
|
|
# |........1.........2.........3.........4.........5.........6.........7.........8
|
|
cat <<-EOF
|
|
$NAME v$VERSION.
|
|
Copyright (c) 2018-2020 Darren 'Tadgy' Austin <darren (at) afterdark.org.uk>.
|
|
Licensed under the terms of the GNU GPL v3 <http://gnu.org/licenses/gpl.html>.
|
|
This program is free software; you can modify or redistribute it in accordence
|
|
with the GNU GPL. However, it comes with ABSOLUTELY NO WARRANTY; not even the
|
|
implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
EOF
|
|
}
|
|
|
|
exit_handler() {
|
|
(( INPUTFD != 0 )) && { exec {INPUTFD}>&-; } 2>/dev/null
|
|
|
|
(( FLAGS[created-fifo] == 1 )) && {
|
|
rm -f "$INPUT" 2>/dev/null || syslog "warn" "failed to remove pipe/fifo: $INPUT"
|
|
}
|
|
}
|
|
|
|
is_dir() {
|
|
# $1 The path to verify is a directory.
|
|
|
|
[[ -z "$1" ]] && return 1
|
|
|
|
[[ ! -d "$1" ]] && return 1
|
|
return 0
|
|
}
|
|
|
|
make_dir() {
|
|
# $1 The directory to create.
|
|
|
|
[[ -z "$1" ]] && return 1
|
|
|
|
if [[ ! -e "$1" ]]; then
|
|
umask "$DIR_UMASK"
|
|
mkdir -p "$1" 2>/dev/null || return 1
|
|
else
|
|
is_dir "$1" || return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
open_fd() {
|
|
# $1 The site/vhost identifier in the array.
|
|
# $2 The log file path to open.
|
|
|
|
[[ -z "$1" || -z "$2" ]] && return 1
|
|
umask "$FILE_UMASK"
|
|
# shellcheck disable=SC1083
|
|
if ! { exec {FDS[$1]}>>"$2"; } 2>/dev/null; then
|
|
(( FLAGS[${1}_open-fd-fail] == 0 )) && {
|
|
syslog "error" "failed to open log file for writing: $2"
|
|
FLAGS[${1}_open-fd-fail]=1
|
|
}
|
|
return 1
|
|
else
|
|
(( FLAGS[${1}_open-fd-fail] == 1 )) && {
|
|
syslog "info" "opened log file for writing: $2"
|
|
FLAGS[${1}_open-fd-fail]=0
|
|
}
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
remove_expansions() {
|
|
# This function takes a template path as input and will output a path with elements
|
|
# starting with the first (non-escaped) %-sequence to the end of the path, removed.
|
|
# That is, it will return the path with the variable parts removed.
|
|
# $1 The path to parse.
|
|
|
|
local IFS='/' ITEMS INDEX
|
|
|
|
read -r -a ITEMS <<<"${1//+(\/)/\/}"
|
|
|
|
for INDEX in "${!ITEMS[@]}"; do
|
|
# Thanks to Marc Eberhard for helping with the regex that I couldn't quite get right.
|
|
if [[ "${ITEMS[INDEX]}" =~ ^((%%)*[^%]*)*[%]?$ ]]; then
|
|
printf "%s" "${ITEMS[INDEX]}"
|
|
[[ -n "${ITEMS[INDEX+1]}" ]] && [[ "${ITEMS[INDEX+1]}" =~ ^((%%)*[^%]*)*[%]?$ ]] && printf "%s" "/"
|
|
else
|
|
break
|
|
fi
|
|
done
|
|
}
|
|
|
|
sigchld_handler() {
|
|
local JOB
|
|
|
|
for JOB in "${!JOBS[@]}"; do
|
|
[[ "${JOBS[$JOB]}" ]] && {
|
|
! kill -0 "${JOBS[$JOB]}" >/dev/null 2>&1 && {
|
|
wait "${JOBS[$JOB]}"
|
|
unset "JOBS[$JOB]"
|
|
(( RUNNING_JOBS-- ))
|
|
}
|
|
}
|
|
done
|
|
start_compression_jobs
|
|
(( RUNNING_JOBS == 0 )) && set +bm
|
|
}
|
|
|
|
sighup_handler() {
|
|
local SITE
|
|
|
|
syslog "info" "closing all file descriptors"
|
|
for SITE in "${!FDS[@]}"; do
|
|
close_fd "$SITE"
|
|
done
|
|
}
|
|
|
|
sigterm_handler() {
|
|
local SITE
|
|
|
|
for SITE in "${!FDS[@]}"; do
|
|
close_fd "$SITE"
|
|
done
|
|
disown -a
|
|
exit 0
|
|
}
|
|
|
|
start_compression_jobs() {
|
|
local JOB
|
|
|
|
while (( RUNNING_JOBS < MAXJOBS )); do
|
|
for JOB in "${!JOBS[@]}"; do
|
|
[[ ! "${JOBS[$JOB]}" ]] && {
|
|
set -bm
|
|
"$COMPRESSOR" "${COMPRESSOR_ARGS[@]}" "$JOB" >/dev/null 2>&1 &
|
|
JOBS[$JOB]="$!"
|
|
(( RUNNING_JOBS++ ))
|
|
continue 2
|
|
}
|
|
done
|
|
break
|
|
done
|
|
}
|
|
|
|
syslog() {
|
|
# $1 The syslog level at which to log the message.
|
|
# $2 The text of the message to log.
|
|
|
|
[[ -z "$1" || -z "$2" ]] && return 1
|
|
|
|
logger --id="$$" -p "$SYSLOG_FACILITY.$1" -t "$NAME" "$1: $2" 2>/dev/null
|
|
}
|
|
|
|
|
|
# Extended globs are required.
|
|
shopt -s extglob
|
|
|
|
# Some variables.
|
|
# The array of file descriptors corresponding to each path.
|
|
declare -A FDS
|
|
# The array of jobs needing to be compressed.
|
|
declare -A JOBS
|
|
# The array of flags.
|
|
declare -A FLAGS
|
|
# The number of compression jobs currently active.
|
|
RUNNING_JOBS=0
|
|
# The original arguments to the script.
|
|
ORIG_ARGS=()
|
|
|
|
# Some detaults.
|
|
COMPRESSOR_ARGS=( "-9" )
|
|
COMPRESSOR="gzip" # Use gzip by default as log processing utils can often natively read gzipped files.
|
|
INPUT=""
|
|
INPUTFD="0"
|
|
MAXJOBS="4"
|
|
LINKFILE=""
|
|
DIR_UMASK="022"
|
|
FILE_UMASK="022"
|
|
PIPE_UMASK="066"
|
|
PIPE_OWNER=""
|
|
SYSLOG_FACILITY="user"
|
|
RUNAS_USER=""
|
|
FLAGS=([flush]=0 [raw]=0 [compress]=0 [make-parents]=0 [created-fifo]=0 [timed-out]=0 [basedir-vanished]=0 [basedir-notdir]=0)
|
|
|
|
# trap signals.
|
|
trap 'sigchld_handler' SIGCHLD
|
|
trap 'sighup_handler' SIGHUP
|
|
trap 'syslog "info" "received SIGUSR1 ping request"' SIGUSR1
|
|
trap 'sigterm_handler' SIGTERM
|
|
trap 'exit_handler' EXIT
|
|
|
|
# Parse command line options.
|
|
while :; do
|
|
case "$1" in
|
|
-ca)
|
|
# Set the compression command arguments.
|
|
[[ ! "$2" ]] && die "missing argument to -ca"
|
|
read -r -a COMPRESSOR_ARGS <<<"$2"
|
|
ORIG_ARGS+=("$1" "$2")
|
|
shift 2
|
|
continue
|
|
;;
|
|
-cc)
|
|
# Set the compression command to use.
|
|
[[ ! "$2" ]] && die "missing argument to -cc"
|
|
"$2" --help >/dev/null 2>&1 || die "invalid compressor command: $2"
|
|
COMPRESSOR="$2"
|
|
ORIG_ARGS+=("$1" "$2")
|
|
shift 2
|
|
continue
|
|
;;
|
|
-f)
|
|
# Flush files after every write.
|
|
FLAGS[flush]=1
|
|
ORIG_ARGS+=("$1")
|
|
shift
|
|
continue
|
|
;;
|
|
-h|-help|--help)
|
|
# Show the help screen and exit.
|
|
display_help
|
|
exit 0
|
|
;;
|
|
-i)
|
|
# Use a pipe/FIFO instead of stdin.
|
|
[[ ! "$2" ]] && die "missing argument to -i"
|
|
[[ "${2:0:1}" != "/" ]] && die "must be an absolute path for -i: $2"
|
|
INPUT="$2"
|
|
ORIG_ARGS+=("$1" "$2")
|
|
shift 2
|
|
continue
|
|
;;
|
|
-j)
|
|
# Set the maximum number of concurrent compression jobs to have active at once.
|
|
[[ ! "$2" =~ [0-9]+ ]] && die "invalid number of jobs: $2"
|
|
(( $2 == 0 )) && die "invalid number of jobs: $2"
|
|
MAXJOBS="$2"
|
|
ORIG_ARGS+=("$1" "$2")
|
|
shift 2
|
|
continue
|
|
;;
|
|
-l)
|
|
# Set the link name to use.
|
|
[[ ! "$2" ]] && die "missing argument to -l"
|
|
[[ "${2:0:1}" == "/" ]] && die "link name cannot begin with '/': $2"
|
|
[[ "${2: -1:1}" == "/" ]] && die "link name cannot end with '/': $2"
|
|
LINKFILE="$2"
|
|
ORIG_ARGS+=("$1" "$2")
|
|
shift 2
|
|
continue
|
|
;;
|
|
-md)
|
|
# Set the directory umask.
|
|
[[ ! "$2" ]] && die "missing argument to -md"
|
|
[[ ! "$2" =~ [0-7]{3,4} ]] && die "invalid umask given for -md: $2"
|
|
DIR_UMASK="$2"
|
|
ORIG_ARGS+=("$1" "$2")
|
|
shift 2
|
|
continue
|
|
;;
|
|
-mf)
|
|
# Set the file umask.
|
|
[[ ! "$2" ]] && die "missing argument to -mf"
|
|
[[ ! "$2" =~ [0-7]{3} ]] && die "invalid umask given for -mf: $2"
|
|
FILE_UMASK="$2"
|
|
ORIG_ARGS+=("$1" "$2")
|
|
shift 2
|
|
continue
|
|
;;
|
|
-mp)
|
|
# Set the pipe umask.
|
|
[[ ! "$2" ]] && die "missing argument to -mp"
|
|
[[ ! "$2" =~ [0-7]{3} ]] && die "invalid umask given for -mp: $2"
|
|
PIPE_UMASK="$2"
|
|
shift 2
|
|
continue
|
|
;;
|
|
-o)
|
|
# Set the ownership of the pipe/FIFO.
|
|
(( UID != 0 )) && die "only root can use -o"
|
|
[[ ! "$2" ]] && die "missing argument to -o"
|
|
[[ ! "$2" =~ ^.*:.*$ ]] && die "option -o must include a ':': $2"
|
|
[[ -z "${2%%:*}" ]] && [[ -z "${2##*:}" ]] && die "both owner and group missing from -o: $2"
|
|
[[ -n "${2%%:*}" ]] && { getent passwd "${2%%:*}" >/dev/null 2>&1 || die "invalid user part given for -o: $2"; }
|
|
[[ -n "${2##*:}" ]] && { getent group "${2##*:}" >/dev/null 2>&1 || die "invalid group part given for -o: $2"; }
|
|
PIPE_OWNER="$2"
|
|
shift 2
|
|
continue
|
|
;;
|
|
-p)
|
|
# Create parent directories.
|
|
FLAGS[make-parents]=1
|
|
ORIG_ARGS+=("$1")
|
|
shift
|
|
continue
|
|
;;
|
|
-r)
|
|
# Set raw mode.
|
|
FLAGS[raw]=1
|
|
ORIG_ARGS+=("$1")
|
|
shift
|
|
continue
|
|
;;
|
|
-s)
|
|
# Set the syslog facility.
|
|
[[ ! "${2,,}" =~ (auth|authpriv|cron|daemon|ftp|kern|lpr|mail|news|syslog|user|uucp|local[0-7]) ]] && die "invalid syslog facility: $2"
|
|
SYSLOG_FACILITY="${2,,}"
|
|
ORIG_ARGS+=("$1" "$2")
|
|
shift 2
|
|
continue
|
|
;;
|
|
-u)
|
|
# Set the user to run as.
|
|
(( UID != 0 )) && die "only root can use -u"
|
|
getent passwd "$2" >/dev/null 2>&1 || die "invalid user given for -u: $2"
|
|
RUNAS_USER="$2"
|
|
shift 2
|
|
continue
|
|
;;
|
|
-v)
|
|
# Show the version and exit.
|
|
display_version
|
|
exit 0
|
|
;;
|
|
-z)
|
|
# Compress logs once they are rotated.
|
|
FLAGS[compress]=1
|
|
ORIG_ARGS+=("$1")
|
|
shift
|
|
continue
|
|
;;
|
|
--)
|
|
# Stop processing options. Everything further is taken as an argument.
|
|
shift
|
|
break
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# If there isn't 2 arguments left, exit.
|
|
(( $# != 2 )) && {
|
|
printf "%s\\n" "$NAME: incorrect number of non-option arguments" >&2
|
|
printf "%s\\n" "Try: $NAME -h" >&2
|
|
exit 1
|
|
}
|
|
|
|
# The remaining arguments should be the base directory and the template.
|
|
BASEDIR="${1/%\//}"
|
|
TEMPLATE="$2"
|
|
|
|
# Santy checking.
|
|
[[ "${BASEDIR:0:1}" != "/" ]] && die "must be an absolute path: $BASEDIR"
|
|
[[ ! -e "$BASEDIR" ]] && die "base directory does not exist: $BASEDIR"
|
|
[[ ! -d "$BASEDIR" ]] && die "not a directory: $BASEDIR"
|
|
(( "${FLAGS[make-parents]}" == 1 )) && [[ ! -w "$BASEDIR" ]] && die "no write permission: $BASEDIR"
|
|
[[ "${TEMPLATE: 0:1}" == "/" ]] && die "template cannot start with '/' - must be a relative path: $TEMPLATE"
|
|
[[ "${TEMPLATE: -1:1}" == "/" ]] && die "template cannot end with '/' - path must be a filename: $TEMPLATE"
|
|
(( FLAGS[raw] == 0 )) && [[ ! "$TEMPLATE" =~ .*\{\} ]] && die "template must include at least one '{}': $TEMPLATE"
|
|
(( FLAGS[raw] != 0 )) && [[ "$TEMPLATE" =~ .*\{\} ]] && die "template cannot include '{}': $TEMPLATE"
|
|
(( FLAGS[raw] != 0 )) && [[ "$LINKFILE" =~ .*\{\} ]] && die "link name cannot include '{}': $LINKFILE"
|
|
|
|
# If input is to be a pipe/FIFO, create it if necessary.
|
|
[[ -n "$INPUT" ]] && {
|
|
if [[ ! -e "$INPUT" ]]; then
|
|
umask "$PIPE_UMASK"
|
|
mkfifo "$INPUT" 2>/dev/null || die "failed to create pipe/FIFO: $INPUT"
|
|
FLAGS[created-fifo]=1
|
|
[[ -n "$PIPE_OWNER" ]] && { chown "$PIPE_OWNER" "$INPUT" >/dev/null 2>&1 || die "failed to chown pipe/FIFO: $INPUT"; }
|
|
elif [[ ! -p "$INPUT" ]]; then
|
|
die "not a pipe/FIFO: $INPUT"
|
|
fi
|
|
}
|
|
|
|
# Apply user and setting.
|
|
# shellcheck disable=SC2093
|
|
[[ -n "$RUNAS_USER" ]] && { exec -a "su" /bin/su - "$RUNAS_USER" -- "$0" "${ORIG_ARGS[@]}" "$BASEDIR" "$TEMPLATE" || die "failed to exec to change user"; }
|
|
|
|
# If input is to be a pipe/FIFO, open it.
|
|
[[ -n "$INPUT" ]] && { exec {INPUTFD}<"$INPUT" || die "failed to open pipe/FIFO for reading: $INPUT"; }
|
|
|
|
# Main loop
|
|
while :; do
|
|
# Reset used variables.
|
|
unset LOG_VHOST LOG_DATA
|
|
FLAGS[timed-out]=0
|
|
|
|
# Start compression jobs if there's any in the queue.
|
|
start_compression_jobs
|
|
|
|
# The time until the top of the next minute - this is used for the 'read' timeout so that
|
|
# closing log files and compression can still occur even if no log lines are written.
|
|
# Note: This does mean we can't have per second log files, but I can't see that being a requirement.
|
|
# shellcheck disable=SC2183
|
|
TTNM="$(( 60 - 10#$(printf "%(%S)T") ))"
|
|
|
|
# Read the log line.
|
|
# Note: The $(...) expansion should *not* be quoted in this instance, and the space between
|
|
# $( and (( is necessary to quiet shellcheck.
|
|
# shellcheck disable=SC2046
|
|
read -r -t "$TTNM" -u "$INPUTFD" $( (( FLAGS[raw] == 0 )) && printf "%s" "LOG_VHOST") LOG_DATA
|
|
ERR="$?"
|
|
|
|
# Determine how the read above was exited.
|
|
if (( ERR > 128 )); then
|
|
# If 'read' timed out, set a marker.
|
|
FLAGS[timed-out]=1
|
|
elif (( ERR == 1 )); then
|
|
(( INPUTFD == 0 )) && {
|
|
# stdin has been closed by the parent, quit gracefully by raising a SIGTERM.
|
|
kill -TERM "$$"
|
|
}
|
|
elif (( ERR != 0 )); then
|
|
# Unhandled error - log the issue and continue.
|
|
syslog "error" "unhandled return code from 'read': $ERR"
|
|
continue
|
|
fi
|
|
|
|
# Make sure the base path still exists - it could have disappeared while we were blocked in 'read'.
|
|
# Note: We won't make this directory ourselves - as it's the base directory it should exist on the system to start with.
|
|
if [[ ! -e "$BASEDIR" ]]; then
|
|
(( FLAGS[basedir-vanished] == 0 )) && {
|
|
syslog "error" "base directory has vanished"
|
|
FLAGS[basedir-vanished]=1
|
|
}
|
|
continue
|
|
else
|
|
(( FLAGS[basedir-vanished] == 1 )) && {
|
|
syslog "info" "base directory has reappeared"
|
|
FLAGS[basedir-vanished]=0
|
|
}
|
|
fi
|
|
|
|
# Make sure the base path is a directory.
|
|
if ! is_dir "$BASEDIR"; then
|
|
(( FLAGS[basedir-notdir] == 0 )) && {
|
|
syslog "error" "base path is no longer a directory"
|
|
FLAGS[basedir-notdir]=1
|
|
}
|
|
continue
|
|
else
|
|
(( FLAGS[basedir-notdir] == 1 )) && {
|
|
syslog "info" "base path has become directory again"
|
|
FLAGS[basedir-notdir]=0
|
|
}
|
|
fi
|
|
|
|
# Expand the strftime-encoded strings in the template.
|
|
EXPANDED_TEMPLATE="$(printf "%($TEMPLATE)T")"
|
|
|
|
# The old expanded template needs to be seeded if it's not already set from a previous loop.
|
|
# Set it to the same as the current expanded template so that no rotation is done the first time around.
|
|
[[ -z "$OLD_TEMPLATE" ]] && OLD_TEMPLATE="$EXPANDED_TEMPLATE"
|
|
|
|
# If the 'read' timed out and the exapnded template is the same as the old expanded template, there is no need to do anything.
|
|
(( FLAGS[timed-out] == 1 )) && [[ "$EXPANDED_TEMPLATE" == "$OLD_TEMPLATE" ]] && continue
|
|
|
|
# If the 'read' did not time out but the line read is empty, don't do anything.
|
|
(( FLAGS[timed-out] == 0 )) && [[ "$LOG_DATA" =~ ^[[:space:]]*$ ]] && continue
|
|
|
|
# If the new expanded template is different from the old, close and reopen all the logs and queue for compression (if required).
|
|
[[ "$EXPANDED_TEMPLATE" != "$OLD_TEMPLATE" ]] && {
|
|
# Loop through all the open FDs.
|
|
for SITE in "${!FDS[@]}"; do
|
|
# Generate the fully expanded filename from the strftime-expanded template and the site name from the array.
|
|
FILENAME="$BASEDIR/${EXPANDED_TEMPLATE//\{\}/$SITE}"
|
|
|
|
# Close the file descriptor for the old log file path.
|
|
close_fd "$SITE"
|
|
|
|
# Make sure the directory leading up to the expanded part of the template exists.
|
|
# Note: We don't create this part of the template's path tree as doing so would
|
|
# potentially allow any path to be created by use of a custom Host: header.
|
|
check_leading_dirs "$SITE" "$BASEDIR/${TEMPLATE//\{\}/$SITE}" || continue
|
|
|
|
# Create what's missing from the full log file's directory based on the expanded template.
|
|
create_missing_dirs "$SITE" "${FILENAME%/*}" || continue
|
|
|
|
# Open the new log file.
|
|
open_fd "$SITE" "$FILENAME" || continue
|
|
|
|
# Fix the now broken symlink - point it to the currently active log file.
|
|
[[ "$LINKFILE" ]] && {
|
|
LINKFILE_EXPANDED="$(printf "%($LINKFILE)T")"
|
|
# Note: This will clobber anything that already exists with the link name.
|
|
rm -rf "${BASEDIR:?}/${LINKFILE_EXPANDED//\{\}/$SITE}"
|
|
if ! ln -sfr "$FILENAME" "$BASEDIR/${LINKFILE_EXPANDED//\{\}/$SITE}"; then
|
|
(( FLAGS[${SITE}_fix-link] == 0 )) && {
|
|
syslog "error" "failed to fix link: $BASEDIR/${LINKFILE_EXPANDED//\{\}/$SITE}"
|
|
FLAGS[${SITE}_fix-link]=1
|
|
}
|
|
continue
|
|
else
|
|
(( FLAGS[${SITE}_fix-link] == 1 )) && {
|
|
syslog "info" "fixed link: $BASEDIR/${LINKFILE_EXPANDED//\{\}/$SITE}"
|
|
FLAGS[${SITE}_fix-link]=0
|
|
}
|
|
fi
|
|
}
|
|
# Add the old log file to the compression jobs task list.
|
|
(( FLAGS[compress] != 0 )) && {
|
|
JOBS+=([$BASEDIR/${OLD_TEMPLATE//\{\}/$SITE}]="")
|
|
}
|
|
done
|
|
}
|
|
|
|
# If the 'read' did not time out, there must be a log line to write.
|
|
(( FLAGS[timed-out] == 0 )) && {
|
|
# If not in raw mode, an unset LOG_VHOST is an error.
|
|
# If in raw mode, we need a placeholder for the FDS array element as LOG_VHOST would normally be unset.
|
|
if (( FLAGS[raw] == 0 )); then
|
|
[[ ! "$LOG_VHOST" ]] && {
|
|
syslog "error" "empty VirtualHost site identifier"
|
|
continue
|
|
}
|
|
else
|
|
LOG_VHOST="_raw_"
|
|
fi
|
|
|
|
# Generate the fully expanded filename from the strftime-expanded template.
|
|
FILENAME="$BASEDIR/${EXPANDED_TEMPLATE//\{\}/$LOG_VHOST}"
|
|
|
|
# Unless the -p option has been used, make sure the directory leading up to the
|
|
# expanded part of the template exists.
|
|
(( FLAGS[make-parents] == 0 )) && {
|
|
check_leading_dirs "$LOG_VHOST" "$BASEDIR/${TEMPLATE//\{\}/$LOG_VHOST}" || continue
|
|
}
|
|
|
|
# Create what's missing from the full log file's directory based on the expanded template.
|
|
create_missing_dirs "$LOG_VHOST" "${FILENAME%/*}" || continue
|
|
|
|
# If no FD is open for the LOG_VHOST, open it.
|
|
[[ -z "${FDS[$LOG_VHOST]}" ]] && {
|
|
open_fd "$LOG_VHOST" "$FILENAME" || continue
|
|
}
|
|
|
|
# Write the log entry.
|
|
printf "%s\\n" "$LOG_DATA" >&"${FDS[$LOG_VHOST]}"
|
|
|
|
# Flush data to disk if requested.
|
|
(( FLAGS[flush] == 1 )) && {
|
|
if ! sync "$FILENAME" 2>/dev/null; then
|
|
(( FLAGS[sync-fail] == 0 )) && {
|
|
syslog "warn" "failed to sync: $FILENAME"
|
|
FLAGS[sync-fail]=1
|
|
}
|
|
continue
|
|
else
|
|
(( FLAGS[sync-fail] == 1 )) && {
|
|
syslog "info" "sync successful: $FILENAME"
|
|
FLAGS[sync-fail]=0
|
|
}
|
|
fi
|
|
}
|
|
|
|
# Create symlink to the currently active log file.
|
|
[[ "$LINKFILE" ]] && {
|
|
LINKFILE_EXPANDED="$(printf "%($LINKFILE)T")"
|
|
[[ "$(stat -L --printf="%d:%i" "$BASEDIR/${LINKFILE_EXPANDED//\{\}/$LOG_VHOST}" 2>/dev/null)" != \
|
|
"$(stat --printf="%d:%i" "$FILENAME" 2>/dev/null)" ]] && {
|
|
# Note: This will clobber anything that already exists with the link name.
|
|
rm -rf "${BASEDIR:?}/${LINKFILE_EXPANDED//\{\}/$LOG_VHOST}"
|
|
if ! ln -sfr "$FILENAME" "$BASEDIR/${LINKFILE_EXPANDED//\{\}/$LOG_VHOST}" 2>/dev/null; then
|
|
(( FLAGS[${LOG_VHOST}_create-link] == 0 )) && {
|
|
syslog "error" "failed to create link: $BASEDIR/${LINKFILE_EXPANDED//\{\}/$LOG_VHOST}"
|
|
FLAGS[${LOG_VHOST}_create-link]=1
|
|
}
|
|
continue
|
|
else
|
|
(( FLAGS[${LOG_VHOST}_create-link] == 1 )) && {
|
|
syslog "info" "created link: $BASEDIR/${LINKFILE_EXPANDED//\{\}/$LOG_VHOST}"
|
|
FLAGS[${LOG_VHOST}_create-link]=0
|
|
}
|
|
fi
|
|
}
|
|
}
|
|
}
|
|
|
|
# Store the last used filename.
|
|
OLD_TEMPLATE="$EXPANDED_TEMPLATE"
|
|
done
|