Files
linux-scripts/webhook-relay-server.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

680 lines
22 KiB
Bash

#!/bin/bash
# ============================================================================
# Webhook Relay Server
# Installs and configures a webhook relay server using adnanh/webhook
# ============================================================================
# Author : Phil Connor
# Contact : contact@mylinux.work
# License : MIT
# Version : 1.0.0
# ============================================================================
set -euo pipefail
# ── Defaults ─────────────────────────────────────────────────────────────────
WEBHOOK_VERSION="${WEBHOOK_VERSION:-2.8.2}"
WEBHOOK_PORT="${WEBHOOK_PORT:-9000}"
WEBHOOK_USER="webhook"
WEBHOOK_BIN="/usr/local/bin/webhook"
WEBHOOK_CONF_DIR="/etc/webhook"
WEBHOOK_HOOKS_FILE="${WEBHOOK_CONF_DIR}/hooks.json"
WEBHOOK_HANDLER_DIR="${WEBHOOK_CONF_DIR}/handlers"
WEBHOOK_LOG_DIR="/var/log/webhook"
WEBHOOK_SECRET="${WEBHOOK_SECRET:-}"
PROM_TEXTFILE_DIR="/var/lib/node_exporter/textfile"
PROM_FILE="${PROM_TEXTFILE_DIR}/webhook.prom"
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
MODE="install"
# ── Colors ───────────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" RESET=""
fi
# ── Logging ──────────────────────────────────────────────────────────────────
info() { echo -e "${GREEN}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
die() { err "$*"; exit 1; }
section() {
echo ""
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
echo ""
}
field() {
printf " ${BOLD}%-24s${RESET} %s\n" "$1" "$2"
}
# ── Usage ────────────────────────────────────────────────────────────────────
show_help() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS]
Install and configure a webhook relay server using adnanh/webhook.
OPTIONS:
--port PORT Listen port (default: 9000)
--secret SECRET Webhook HMAC secret for hooks.json
--version VER webhook binary version (default: 2.8.2)
--status Show service status and configuration
--uninstall Remove webhook server and all configuration
--help Show this help
ENVIRONMENT:
WEBHOOK_VERSION Override binary version
WEBHOOK_PORT Override listen port
WEBHOOK_SECRET HMAC secret for hook validation
EXAMPLES:
sudo $SCRIPT_NAME
sudo $SCRIPT_NAME --port 8080 --secret my-secret
sudo $SCRIPT_NAME --status
sudo $SCRIPT_NAME --uninstall
EOF
}
# ── Argument parsing ─────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--port) WEBHOOK_PORT="$2"; shift 2 ;;
--secret) WEBHOOK_SECRET="$2"; shift 2 ;;
--version) WEBHOOK_VERSION="$2"; shift 2 ;;
--status) MODE="status"; shift ;;
--uninstall) MODE="uninstall"; shift ;;
--help|-h) show_help; exit 0 ;;
*) die "Unknown option: $1 (see --help)" ;;
esac
done
# ── Preflight ────────────────────────────────────────────────────────────────
check_root() {
[[ $EUID -eq 0 ]] || die "This script must be run as root"
}
detect_os() {
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
OS_ID="${ID}"
OS_VERSION="${VERSION_ID:-}"
else
die "Cannot detect OS — /etc/os-release not found"
fi
case "$OS_ID" in
debian|ubuntu)
OS_FAMILY="debian"
;;
rhel|centos|almalinux|rocky|fedora)
OS_FAMILY="rhel"
;;
*)
die "Unsupported OS: $OS_ID"
;;
esac
info "Detected OS: ${OS_ID} ${OS_VERSION} (${OS_FAMILY})"
}
detect_arch() {
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) WEBHOOK_ARCH="amd64" ;;
aarch64) WEBHOOK_ARCH="arm64" ;;
armv7l) WEBHOOK_ARCH="armhf" ;;
*) die "Unsupported architecture: $ARCH" ;;
esac
}
check_dependencies() {
local missing=()
for cmd in curl jq tar; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
info "Installing missing dependencies: ${missing[*]}"
if [[ "$OS_FAMILY" == "debian" ]]; then
apt-get update -qq
apt-get install -y -qq "${missing[@]}"
else
dnf install -y -q "${missing[@]}"
fi
fi
}
# ── Install webhook binary ──────────────────────────────────────────────────
install_binary() {
section "Installing webhook binary"
if [[ -f "$WEBHOOK_BIN" ]]; then
local current_ver
current_ver=$("$WEBHOOK_BIN" -version 2>&1 || echo "unknown")
info "Existing binary found: $current_ver"
fi
local download_url="https://github.com/adnanh/webhook/releases/download/${WEBHOOK_VERSION}/webhook-linux-${WEBHOOK_ARCH}.tar.gz"
local tmp_dir
tmp_dir="$(mktemp -d)"
info "Downloading webhook ${WEBHOOK_VERSION} (${WEBHOOK_ARCH})..."
if ! curl -fsSL "$download_url" -o "${tmp_dir}/webhook.tar.gz"; then
rm -rf "$tmp_dir"
die "Failed to download webhook from $download_url"
fi
tar -xzf "${tmp_dir}/webhook.tar.gz" -C "$tmp_dir"
local extracted
extracted=$(find "$tmp_dir" -name webhook -type f | head -1)
if [[ -z "$extracted" ]]; then
rm -rf "$tmp_dir"
die "webhook binary not found in archive"
fi
install -m 0755 "$extracted" "$WEBHOOK_BIN"
rm -rf "$tmp_dir"
info "Installed: $WEBHOOK_BIN"
field "Version" "$WEBHOOK_VERSION"
field "Architecture" "$WEBHOOK_ARCH"
}
# ── Create user ──────────────────────────────────────────────────────────────
create_user() {
if id "$WEBHOOK_USER" &>/dev/null; then
info "User $WEBHOOK_USER already exists"
return
fi
useradd --system --no-create-home --shell /usr/sbin/nologin "$WEBHOOK_USER"
info "Created system user: $WEBHOOK_USER"
}
# ── Directory structure ──────────────────────────────────────────────────────
create_directories() {
section "Creating directory structure"
mkdir -p "$WEBHOOK_CONF_DIR" "$WEBHOOK_HANDLER_DIR" "$WEBHOOK_LOG_DIR" "$PROM_TEXTFILE_DIR"
chown "$WEBHOOK_USER":"$WEBHOOK_USER" "$WEBHOOK_LOG_DIR"
chown "$WEBHOOK_USER":"$WEBHOOK_USER" "$PROM_TEXTFILE_DIR"
field "Config" "$WEBHOOK_CONF_DIR"
field "Handlers" "$WEBHOOK_HANDLER_DIR"
field "Logs" "$WEBHOOK_LOG_DIR"
field "Metrics" "$PROM_TEXTFILE_DIR"
}
# ── hooks.json ───────────────────────────────────────────────────────────────
create_hooks_config() {
section "Creating hooks.json"
local secret="${WEBHOOK_SECRET:-CHANGE_ME}"
if [[ -f "$WEBHOOK_HOOKS_FILE" ]]; then
warn "hooks.json already exists — backing up to hooks.json.bak"
cp "$WEBHOOK_HOOKS_FILE" "${WEBHOOK_HOOKS_FILE}.bak"
fi
cat > "$WEBHOOK_HOOKS_FILE" <<HOOKS
[
{
"id": "mirror-sync",
"execute-command": "${WEBHOOK_HANDLER_DIR}/mirror-sync.sh",
"command-working-directory": "/tmp",
"pass-arguments-to-command": [
{ "source": "payload", "name": "repository.full_name" }
],
"trigger-rule": {
"match": {
"type": "payload-hmac-sha256",
"secret": "${secret}",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
}
},
{
"id": "deploy",
"execute-command": "${WEBHOOK_HANDLER_DIR}/deploy.sh",
"command-working-directory": "/tmp",
"pass-arguments-to-command": [
{ "source": "payload", "name": "repository.full_name" },
{ "source": "payload", "name": "ref" }
],
"trigger-rule": {
"and": [
{
"match": {
"type": "payload-hmac-sha256",
"secret": "${secret}",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
},
{
"match": {
"type": "value",
"value": "refs/heads/main",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
]
}
},
{
"id": "notify",
"execute-command": "${WEBHOOK_HANDLER_DIR}/notify.sh",
"command-working-directory": "/tmp",
"pass-arguments-to-command": [
{ "source": "payload", "name": "repository.full_name" },
{ "source": "payload", "name": "sender.login" },
{ "source": "payload", "name": "head_commit.message" }
],
"trigger-rule": {
"match": {
"type": "payload-hmac-sha256",
"secret": "${secret}",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
}
}
]
HOOKS
chmod 640 "$WEBHOOK_HOOKS_FILE"
chown root:"$WEBHOOK_USER" "$WEBHOOK_HOOKS_FILE"
if [[ "$secret" == "CHANGE_ME" ]]; then
warn "No --secret provided — hooks.json uses placeholder. Edit ${WEBHOOK_HOOKS_FILE} before use."
fi
info "Created: $WEBHOOK_HOOKS_FILE (3 hooks)"
}
# ── Handler scripts ──────────────────────────────────────────────────────────
create_handlers() {
section "Creating handler scripts"
# ── Metrics helper (sourced by all handlers) ──
cat > "${WEBHOOK_HANDLER_DIR}/metrics.sh" <<'METRICS'
#!/bin/bash
# Prometheus textfile collector helper — sourced by handler scripts
PROM_FILE="${PROM_FILE:-/var/lib/node_exporter/webhook.prom}"
_WEBHOOK_START=$(date +%s%N)
write_metrics() {
local hook_id="$1"
local status="$2"
local end_time
end_time=$(date +%s%N)
local duration
duration=$(echo "scale=3; ($end_time - $_WEBHOOK_START) / 1000000000" | bc 2>/dev/null || echo "0")
local tmp_file="${PROM_FILE}.$$"
local existing=""
[[ -f "$PROM_FILE" ]] && existing=$(cat "$PROM_FILE")
{
echo "$existing"
echo "# HELP webhook_receive_total Total webhook events received"
echo "# TYPE webhook_receive_total counter"
echo "webhook_receive_total{hook=\"${hook_id}\"} 1"
if [[ "$status" == "success" ]]; then
echo "# HELP webhook_success_total Successful handler executions"
echo "# TYPE webhook_success_total counter"
echo "webhook_success_total{hook=\"${hook_id}\"} 1"
echo "# HELP webhook_last_success_timestamp Unix timestamp of last success"
echo "# TYPE webhook_last_success_timestamp gauge"
echo "webhook_last_success_timestamp{hook=\"${hook_id}\"} $(date +%s)"
else
echo "# HELP webhook_failure_total Failed handler executions"
echo "# TYPE webhook_failure_total counter"
echo "webhook_failure_total{hook=\"${hook_id}\"} 1"
fi
echo "# HELP webhook_handler_duration_seconds Handler execution time"
echo "# TYPE webhook_handler_duration_seconds gauge"
echo "webhook_handler_duration_seconds{hook=\"${hook_id}\"} ${duration}"
} > "$tmp_file"
mv "$tmp_file" "$PROM_FILE"
}
METRICS
# ── Mirror sync handler ──
cat > "${WEBHOOK_HANDLER_DIR}/mirror-sync.sh" <<'HANDLER'
#!/bin/bash
# Mirror sync handler — fetches from origin and pushes to mirror remote
set -euo pipefail
source "$(dirname "$0")/metrics.sh"
REPO_NAME="${1:-}"
MIRROR_BASE="/srv/git/mirrors"
HOOK_ID="mirror-sync"
if [[ -z "$REPO_NAME" ]]; then
echo "No repository name received" >&2
write_metrics "$HOOK_ID" "failure"
exit 1
fi
MIRROR_DIR="${MIRROR_BASE}/${REPO_NAME}.git"
if [[ ! -d "$MIRROR_DIR" ]]; then
echo "Mirror not found: $MIRROR_DIR" >&2
write_metrics "$HOOK_ID" "failure"
exit 1
fi
cd "$MIRROR_DIR"
git fetch --prune origin
git push --mirror mirror
write_metrics "$HOOK_ID" "success"
echo "Mirror sync complete: $REPO_NAME"
HANDLER
# ── Deploy handler ──
cat > "${WEBHOOK_HANDLER_DIR}/deploy.sh" <<'HANDLER'
#!/bin/bash
# Deploy handler — pulls latest code and restarts the application service
set -euo pipefail
source "$(dirname "$0")/metrics.sh"
REPO_NAME="${1:-}"
REF="${2:-}"
DEPLOY_BASE="/srv/apps"
HOOK_ID="deploy"
if [[ -z "$REPO_NAME" ]]; then
echo "No repository name received" >&2
write_metrics "$HOOK_ID" "failure"
exit 1
fi
APP_NAME="${REPO_NAME##*/}"
DEPLOY_DIR="${DEPLOY_BASE}/${APP_NAME}"
if [[ ! -d "$DEPLOY_DIR" ]]; then
echo "Deploy directory not found: $DEPLOY_DIR" >&2
write_metrics "$HOOK_ID" "failure"
exit 1
fi
cd "$DEPLOY_DIR"
git pull origin main
if systemctl is-active --quiet "$APP_NAME"; then
systemctl restart "$APP_NAME"
echo "Restarted service: $APP_NAME"
fi
write_metrics "$HOOK_ID" "success"
echo "Deploy complete: $APP_NAME (ref: $REF)"
HANDLER
# ── Notification relay handler ──
cat > "${WEBHOOK_HANDLER_DIR}/notify.sh" <<'HANDLER'
#!/bin/bash
# Notification relay — posts push events to Slack and/or Discord
set -euo pipefail
source "$(dirname "$0")/metrics.sh"
REPO_NAME="${1:-}"
SENDER="${2:-unknown}"
COMMIT_MSG="${3:-no message}"
HOOK_ID="notify"
SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
if [[ -z "$SLACK_WEBHOOK_URL" && -z "$DISCORD_WEBHOOK_URL" ]]; then
echo "No SLACK_WEBHOOK_URL or DISCORD_WEBHOOK_URL configured" >&2
write_metrics "$HOOK_ID" "failure"
exit 1
fi
PAYLOAD="{\"text\":\"[${REPO_NAME}] ${SENDER}: ${COMMIT_MSG}\"}"
if [[ -n "$SLACK_WEBHOOK_URL" ]]; then
curl -sf -X POST -H 'Content-Type: application/json' -d "$PAYLOAD" "$SLACK_WEBHOOK_URL"
fi
if [[ -n "$DISCORD_WEBHOOK_URL" ]]; then
curl -sf -X POST -H 'Content-Type: application/json' -d "$PAYLOAD" "$DISCORD_WEBHOOK_URL"
fi
write_metrics "$HOOK_ID" "success"
echo "Notification sent: $REPO_NAME ($SENDER)"
HANDLER
chmod 750 "${WEBHOOK_HANDLER_DIR}"/*.sh
chown root:"$WEBHOOK_USER" "${WEBHOOK_HANDLER_DIR}"/*.sh
info "Created handler: mirror-sync.sh"
info "Created handler: deploy.sh"
info "Created handler: notify.sh"
info "Created helper: metrics.sh"
}
# ── Systemd service ──────────────────────────────────────────────────────────
create_systemd_service() {
section "Creating systemd service"
cat > /etc/systemd/system/webhook.service <<SERVICE
[Unit]
Description=Webhook Relay Server
Documentation=https://github.com/adnanh/webhook
After=network.target
[Service]
Type=simple
User=${WEBHOOK_USER}
Group=${WEBHOOK_USER}
ExecStart=${WEBHOOK_BIN} -hooks ${WEBHOOK_HOOKS_FILE} -port ${WEBHOOK_PORT} -verbose -logfile ${WEBHOOK_LOG_DIR}/webhook.log
Restart=on-failure
RestartSec=5
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=${WEBHOOK_LOG_DIR} ${PROM_TEXTFILE_DIR}
PrivateTmp=true
[Install]
WantedBy=multi-user.target
SERVICE
systemctl daemon-reload
systemctl enable webhook.service
info "Created and enabled: webhook.service"
field "Listen port" "$WEBHOOK_PORT"
}
# ── Logrotate ────────────────────────────────────────────────────────────────
create_logrotate() {
section "Creating logrotate config"
cat > /etc/logrotate.d/webhook <<LOGROTATE
${WEBHOOK_LOG_DIR}/webhook.log {
weekly
rotate 4
compress
delaycompress
missingok
notifempty
create 640 ${WEBHOOK_USER} ${WEBHOOK_USER}
postrotate
systemctl reload webhook.service 2>/dev/null || true
endscript
}
LOGROTATE
info "Created: /etc/logrotate.d/webhook"
}
# ── Status ───────────────────────────────────────────────────────────────────
show_status() {
section "Webhook Relay Server — Status"
if [[ -f "$WEBHOOK_BIN" ]]; then
local ver
ver=$("$WEBHOOK_BIN" -version 2>&1 || echo "unknown")
field "Binary" "$WEBHOOK_BIN"
field "Version" "$ver"
else
field "Binary" "not installed"
fi
if systemctl is-active --quiet webhook.service 2>/dev/null; then
field "Service" "active (running)"
elif systemctl is-enabled --quiet webhook.service 2>/dev/null; then
field "Service" "enabled (not running)"
else
field "Service" "not installed"
fi
if [[ -f "$WEBHOOK_HOOKS_FILE" ]]; then
local hook_count
hook_count=$(jq 'length' "$WEBHOOK_HOOKS_FILE" 2>/dev/null || echo "?")
field "Hooks config" "${WEBHOOK_HOOKS_FILE} (${hook_count} hooks)"
else
field "Hooks config" "not found"
fi
if [[ -d "$WEBHOOK_HANDLER_DIR" ]]; then
local handler_count
handler_count=$(find "$WEBHOOK_HANDLER_DIR" -name '*.sh' -not -name 'metrics.sh' | wc -l)
field "Handlers" "${handler_count} scripts in ${WEBHOOK_HANDLER_DIR}"
else
field "Handlers" "not found"
fi
field "Log directory" "$WEBHOOK_LOG_DIR"
field "Metrics file" "$PROM_FILE"
echo ""
if systemctl is-active --quiet webhook.service 2>/dev/null; then
systemctl status webhook.service --no-pager -l 2>/dev/null | head -15
fi
}
# ── Uninstall ────────────────────────────────────────────────────────────────
do_uninstall() {
check_root
section "Uninstalling Webhook Relay Server"
if systemctl is-active --quiet webhook.service 2>/dev/null; then
systemctl stop webhook.service
info "Stopped webhook.service"
fi
if systemctl is-enabled --quiet webhook.service 2>/dev/null; then
systemctl disable webhook.service
fi
local items=(
/etc/systemd/system/webhook.service
/etc/logrotate.d/webhook
"$WEBHOOK_BIN"
)
for item in "${items[@]}"; do
if [[ -e "$item" ]]; then
rm -f "$item"
info "Removed: $item"
fi
done
if [[ -d "$WEBHOOK_CONF_DIR" ]]; then
rm -rf "$WEBHOOK_CONF_DIR"
info "Removed: $WEBHOOK_CONF_DIR"
fi
if [[ -d "$WEBHOOK_LOG_DIR" ]]; then
rm -rf "$WEBHOOK_LOG_DIR"
info "Removed: $WEBHOOK_LOG_DIR"
fi
if [[ -f "$PROM_FILE" ]]; then
rm -f "$PROM_FILE"
info "Removed: $PROM_FILE"
fi
if id "$WEBHOOK_USER" &>/dev/null; then
userdel "$WEBHOOK_USER" 2>/dev/null || true
info "Removed user: $WEBHOOK_USER"
fi
systemctl daemon-reload
info "Uninstall complete"
}
# ── Summary ──────────────────────────────────────────────────────────────────
print_summary() {
section "Installation Summary"
field "Binary" "$WEBHOOK_BIN ($WEBHOOK_VERSION)"
field "Config" "$WEBHOOK_HOOKS_FILE"
field "Handlers" "$WEBHOOK_HANDLER_DIR/"
field "Service" "webhook.service (port $WEBHOOK_PORT)"
field "Logs" "$WEBHOOK_LOG_DIR/webhook.log"
field "Metrics" "$PROM_FILE"
field "User" "$WEBHOOK_USER"
echo ""
info "Next steps:"
echo " 1. Edit ${WEBHOOK_HOOKS_FILE} — set your webhook secret"
echo " 2. Configure handler scripts in ${WEBHOOK_HANDLER_DIR}/"
echo " 3. Start the service: systemctl start webhook"
echo " 4. Test: curl -X POST http://localhost:${WEBHOOK_PORT}/hooks/mirror-sync"
echo ""
}
# ── Main ─────────────────────────────────────────────────────────────────────
main() {
case "$MODE" in
status)
show_status
;;
uninstall)
do_uninstall
;;
install)
check_root
detect_os
detect_arch
check_dependencies
install_binary
create_user
create_directories
create_hooks_config
create_handlers
create_systemd_service
create_logrotate
print_summary
;;
esac
}
main