aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore18
-rw-r--r--README.md72
-rw-r--r--extra/Makefile128
-rw-r--r--extra/outbound.conf.apache11
-rw-r--r--extra/outbound.conf.nginx13
-rwxr-xr-xextra/outbound.init174
-rw-r--r--extra/outbound.service21
-rw-r--r--extra/outbound.spec150
-rw-r--r--extra/outbound.sysusers3
-rw-r--r--extra/pip-helper.sh37
-rwxr-xr-xoutbound.bin11
-rw-r--r--outbound.conf.example18
-rw-r--r--outbound.py104
-rw-r--r--outbound.wsgi.ini.dev17
-rw-r--r--outbound.wsgi.ini.example17
15 files changed, 794 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c5a24ad
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+__pycache__/
+.py[cod]
+up/
+upload/
+*.pyc
+.*.swp
+*.conf
+*.ini
+*.log
+debian/.debhelper/
+debian/*debhelper*
+debian/files
+debian/*.substvars
+debian/fuss/
+.cache
+.dbus
+*.pid
+old/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ed5ccc5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,72 @@
+<!--
+ .. File: README.md
+ .. Location: https://bgstack15.ddns.net/cgit/outbound/
+ .. Author: bgstack15
+ .. SPDX-License-Identifier: GPL-3.0
+ .. Startdate: 2022-11-28
+ .. Title: Readme for Outbound project
+ .. Project: outbound
+ .. Purpose: Describe the project
+ .. History:
+ .. Usage:
+ .. Reference: See References heading
+ .. Improvements:
+ .. Documentation: this file
+ .. Dependencies:
+ -->
+# README for outbound
+This project is a small Flask app that merely redirects to the link farther down the URL. The purpose is to enable my web server to record visits to outbound links. This is not a URL shortener or obfuscator. It's a cheap way for me to see what links my site vistors use, without using javascript.
+
+## Upstream
+The original project is at <https://bgstack15.ddns.net/cgit/outbound/>.
+
+## Features/design goals
+
+* redirect only if referer is in whitelist
+* simple web config
+* simple app config
+* send to web server logs the redirects, or else 404s for invalid link requests
+
+## Alternatives
+Probably some "proper" web analytics suite or javascript garbage that I do not want to learn or use.
+
+## Reason for existence
+To show what links on my site are used.
+
+## Using
+
+### Web app
+Run a simple dev environment.
+
+ FLASK_APP=outbound.py FLASK_DEBUG=True flask run --host=0.0.0.0 --extra-files outbound.conf
+
+### Testing the web app with curl
+
+ $ curl --header "Referer: http://d2-03a:5000/" --header 'Debug: true' http://d2-03a:5000/http://server3/
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+ <title>Redirecting...</title>
+ <h1>Redirecting...</h1>
+ <p>You should be redirected automatically to target URL: <a href="http://server3/">http://server3/</a>. If not click the link.
+
+## Improvements
+
+* Provide an apache wsgi config?
+* Provide an nginx wsgi config?
+
+## Dependencies
+For the web app:
+
+* apache with `mod_wsgi`
+* python3-flask
+
+## References
+
+1. [fuss.conf.example](https://bgstack15.ddns.net/cgit/fuss/tree/fuss.conf.example)
+2. [fuss.py](https://bgstack15.ddns.net/cgit/fuss/tree/fuss.py)
+3. [coupons_web.py](https://bgstack15.ddns.net/cgit/coupons/tree/coupons_web.py)
+4. <https://werkzeug.palletsprojects.com/en/2.2.x/serving/#werkzeug.serving.run_simple>
+5. various [fifconfig](https://bgstack15.ddns.net/cgit/fifconfig/tree/) files
+
+### Internal documents
+
+1. ports.txt: outbound 4682/tcp
diff --git a/extra/Makefile b/extra/Makefile
new file mode 100644
index 0000000..dd01eed
--- /dev/null
+++ b/extra/Makefile
@@ -0,0 +1,128 @@
+# File: Makefile for outbound
+# Location: outbound source package
+# Author: bgstack15
+# Startdate: 2022-11-29
+# Title: Makefile for outbound source package
+# Purpose: To use traditional Unix make utility
+# History:
+# Usage:
+# Reference:
+# fifconfig Makefile
+# Improve:
+# add man page?
+# Document:
+# Dependencies:
+# build-devuan:
+
+APPNAME = outbound
+APPVERSION = 0.0.1
+SRCDIR = $(CURDIR)/..# because Makefile is in extra/ inside this repo
+prefix = /usr
+SYSCONFDIR = $(DESTDIR)/etc
+DEFAULTDIR = $(DESTDIR)/etc/sysconfig# for debian use '$(DESTDIR)/etc/default'
+LIBEXECDIR = $(DESTDIR)$(prefix)/libexec
+SHAREDIR = $(DESTDIR)$(prefix)/share
+DOCDIR = $(SHAREDIR)/doc/$(APPNAME)
+APPVARDIR = $(DESTDIR)/var/$(APPNAME)
+MANDIR = $(SHAREDIR)/man
+SYSVDIR = $(SYSCONFDIR)/init.d
+SYSDDIR = $(DESTDIR)$(prefix)/lib/systemd/system
+LOGDIR = $(DESTDIR)/var/log/outbound
+APACHEDIR = $(SYSCONFDIR)/httpd/conf.d# for debian use '$(SYSCONFDIR)/apache2/sites-available'
+NGINXDIR = $(SYSCONFDIR)/nginx/default.d
+CRONDIR = $(SYSCONFDIR)/cron.d
+SBINDIR = $(DESTDIR)$(prefix)/sbin
+
+# variables for deplist
+DEPTYPE = dep
+SEPARATOR = ,
+
+awkbin :=$(shell which awk)
+chmodbin :=$(shell which chmod)
+cpbin :=$(shell which cp)
+echobin :=$(shell which echo)
+falsebin :=$(shell which false)
+findbin :=$(shell which find)
+grepbin :=$(shell which grep)
+gzipbin :=$(shell which gzip)
+installbin :=$(shell which install)
+rmbin :=$(shell which rm)
+rmdirbin :=$(shell which rmdir)
+sedbin :=$(shell which sed)
+sortbin :=$(shell which sort)
+truebin :=$(shell which true)
+uniqbin :=$(shell which uniq)
+xargsbin :=$(shell which xargs)
+
+with_apache ?= NO
+with_nginx ?= YES
+with_init ?= YES
+with_systemd ?= NO
+with_pip_helper ?= NO
+
+all:
+ -@echo "Nothing to build." && ${truebin}
+
+install: install_files
+
+.PHONY: clean install install_files uninstall list deplist deplist_opts
+
+list:
+ @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | ${awkbin} -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | ${sortbin} | ${grepbin} -E -v -e '^[^[:alnum:]]' -e '^$@$$'
+
+deplist:
+ @# deplist 2020-04-18 input must be comma separated
+ @# DEPTYPE( dep , rec , sug ) for depends, recommends, or suggests
+ @if test -z "${DISTRO}" ; then ${echobin} "Please run \`make deplist\` with DISTRO= one of: `make deplist_opts 2>&1 1>/dev/null | ${xargsbin}`. Aborted." 1>&2 ; exit 1 ; fi
+ @if ! ${echobin} "${DEPTYPE}" | grep -qE "^(dep|rec|sug)$$" ; then ${echobin} "Please run \`make deplist\` with DEPTYPE= one of: dep, rec, sug. Undefined will use \`dep\`. Aborted." 1>&2 ; exit 1; fi
+ @${grepbin} -h --exclude-dir='doc' -riIE "\<${DEPTYPE}-" ${SRCDIR} | ${awkbin} -v "domain=${DISTRO}" -v "deptype=${DEPTYPE}" 'tolower($$2) ~ deptype"-"domain {$$1="";$$2="";print}' | tr ',' '\n' | ${sortbin} | ${uniqbin} | ${sedbin} -r -e 's/^\s*//' -e "s/\s*\$$/${SEPARATOR}/" | ${xargsbin}
+
+deplist_opts:
+ @# deplist_opts 2020-04-18 find all available dependency domains
+ @${grepbin} -h -o -riIE '\<(dep|rec|sug)-[^\ :]+:' ${SRCDIR} | ${sedbin} -r -e 's/(dep|rec|sug)-//;' -e 's/:$$//;' | ${sortbin} | ${uniqbin} 1>&2
+
+install_files:
+ ${installbin} -m0755 -d ${LOGDIR} ${APPVARDIR} \
+ ${DOCDIR} ${SBINDIR} ${SYSCONFDIR} ${LIBEXECDIR}/${APPNAME}
+ ${installbin} -m0755 -t ${SBINDIR} ${SRCDIR}/${APPNAME}.bin
+ ${installbin} -m0644 -t ${LIBEXECDIR}/${APPNAME} ${SRCDIR}/${APPNAME}.py
+ ${installbin} -m0644 ${SRCDIR}/${APPNAME}.conf.example ${SYSCONFDIR}/${APPNAME}.conf
+ ${installbin} -m0644 ${SRCDIR}/${APPNAME}.wsgi.ini.example ${SYSCONFDIR}/${APPNAME}.wsgi.ini
+ ${installbin} -m0644 -t ${DOCDIR} ${SRCDIR}/*.md
+ifeq ($(with_apache),YES)
+ ${installbin} -m0755 -d ${APACHEDIR}
+ ${installbin} -m0644 ${SRCDIR}/extra/${APPNAME}.conf.apache ${APACHEDIR}/${APPNAME}.conf
+endif
+ifeq ($(with_nginx),YES)
+ ${installbin} -m0755 -d ${NGINXDIR}
+ ${installbin} -m0644 ${SRCDIR}/extra/${APPNAME}.conf.nginx ${NGINXDIR}/${APPNAME}.conf
+endif
+ifeq ($(with_init),YES)
+ ${installbin} -m0755 -d ${SYSVDIR}
+ ${installbin} -m0755 ${SRCDIR}/extra/${APPNAME}.init ${SYSVDIR}/${APPNAME}
+endif
+ifeq ($(with_systemd),YES)
+ ${installbin} -m0755 -d ${SYSDDIR}
+ ${installbin} -m0644 ${SRCDIR}/extra/outbound.service -t ${SYSDDIR}
+endif
+ifeq ($(with_pip_helper),YES)
+ ${installbin} -m0755 -t ${LIBEXECDIR}/${APPNAME}/ ${SRCDIR}/extra/pip-helper.sh
+endif
+
+uninstall:
+ @${echobin} SRCDIR=${SRCDIR}
+ ${rmbin} -f \
+ ${DOCDIR}/* \
+ ${LIBEXECDIR}/${APPNAME}/* \
+ ${SBINDIR}/${APPNAME}.bin \
+ 1>/dev/null 2>&1 || :
+ ${rmbin} -f ${APACHEDIR}/${APPNAME}.conf || :
+ ${rmbin} -f ${SYSVDIR}/${APPNAME} || :
+
+ # remove all installed directories that are now blank.
+ ${rmdirbin} ${APPVARDIR} \
+ ${SYSVDIR} ${APACHEDIR} ${DOCDIR} \
+ ${LIBEXECDIR}/${APPNAME} ${LOGDIR} 2>/dev/null || :
+
+clean:
+ -@${echobin} "target $@ not implemented yet! Gotta say unh." && ${falsebin}
diff --git a/extra/outbound.conf.apache b/extra/outbound.conf.apache
new file mode 100644
index 0000000..7e95a5a
--- /dev/null
+++ b/extra/outbound.conf.apache
@@ -0,0 +1,11 @@
+# Apache example config for outbound application
+# Needs setsebool -P http_can_network_connect 1
+# vim:set syntax=apache ts=3 sw=3 sts=3 sr et:
+ProxyPass /outbound http://localhost:4682/
+ProxyPassReverse /outbound http://localhost:4682/
+<Location /outbound>
+ # a2enmod headers. These are extra ones that are not provided by Apache natively.
+ RequestHeader set X-Forwarded-Proto "https"
+ # This header is not required to be set manually. The ProxyPass orand Location directive already provide it!
+ #RequestHeader append X-Forwarded-Prefix "/outbound"
+</Location>
diff --git a/extra/outbound.conf.nginx b/extra/outbound.conf.nginx
new file mode 100644
index 0000000..1d4fd49
--- /dev/null
+++ b/extra/outbound.conf.nginx
@@ -0,0 +1,13 @@
+# Nginx example config for outbound application
+# Needs setsebool -P http_can_network_connect 1
+location /outbound/ {
+ #proxy_redirect off;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Script-Name /outbound;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Prefix "/outbound";
+ proxy_pass http://localhost:4682/;
+}
+
diff --git a/extra/outbound.init b/extra/outbound.init
new file mode 100755
index 0000000..dbdc20a
--- /dev/null
+++ b/extra/outbound.init
@@ -0,0 +1,174 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides: outbound
+# Required-Start: $local_fs $network $remote_fs $syslog
+# Required-Stop: $local_fs $network $remote_fs $syslog
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: File Upoad and Storage Service
+# Description: Python3 Flask application that presents a basic file hosting service
+### END INIT INFO
+
+# Author: B. Stack <bgstack15@gmail.com>
+
+# Do NOT "set -e"
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+DAEMON_ARGS=""
+DAEMON=/usr/sbin/outbound.bin
+DESC="outbound"
+NAME=outbound
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+PIDFILE2=/var/$NAME/$NAME-wsgi.pid
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+USER=outbound
+WSGIBIN=uwsgi_python39
+export outbound_WSGI_INI=/etc/outbound.wsgi.ini
+export outbound_CONF=/etc/outbound.conf
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+ # Return
+ # 0 if daemon has been started
+ # 1 if daemon was already running
+ # 2 if daemon could not be started
+ #start-stop-daemon --start --quiet --chuid $USER --group $USER --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
+ # || return 1
+ #start-stop-daemon --start --quiet --chuid $USER --group $USER --background --pidfile $PIDFILE --exec $DAEMON -- \
+ # $DAEMON_ARGS \
+ # || return 2
+ #su $USER -c "$DAEMON" &
+ # The above code will not work for interpreted scripts, use the next
+ # six lines below instead (Ref: #643337, start-stop-daemon(8) )
+ start-stop-daemon --start --make-pidfile --pidfile $PIDFILE \
+ --chuid $USER --group $USER \
+ --startas $DAEMON \
+ --name $WSGIBIN --test > /dev/null \
+ || return 1
+ start-stop-daemon --start --background --make-pidfile --pidfile $PIDFILE --startas $DAEMON \
+ --chuid $USER --group $USER \
+ --name $WSGIBIN -- $DAEMON_ARGS \
+ || return 2
+
+ # Add code here, if necessary, that waits for the process to be ready
+ # to handle requests from services started subsequently which depend
+ # on this one. As a last resort, sleep for some time.
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+ # Return
+ # 0 if daemon has been stopped
+ # 1 if daemon was already stopped
+ # 2 if daemon could not be stopped
+ # other if a failure occurred
+ #start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --user $USER --pidfile $PIDFILE
+ /usr/bin/$WSGIBIN --stop $PIDFILE2 2>/dev/null
+ RETVAL="$?"
+ [ "$RETVAL" = 2 ] && return 2
+ # Wait for children to finish too if this is a daemon that forks
+ # and if the daemon is only ever run from this initscript.
+ # If the above conditions are not satisfied then add some other code
+ # that waits for the process to drop all resources that could be
+ # needed by services started subsequently. A last resort is to
+ # sleep for some time.
+ start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --user $USER --pidfile $PIDFILE
+ [ "$?" = 2 ] && return 2
+ # Many daemons don't delete their pidfiles when they exit.
+ rm -f $PIDFILE
+ return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+ #
+ # If the daemon can reload its configuration without
+ # restarting (for example, when it is sent a SIGHUP),
+ # then implement that here.
+ #
+ start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $WSGIBIN
+ return 0
+}
+
+case "$1" in
+ start)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+ do_start
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ stop)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ status)
+ status_of_proc -p $PIDFILE "$DAEMON" "$NAME" && exit 0 || exit $?
+ ;;
+ #reload|force-reload)
+ #
+ # If do_reload() is not implemented then leave this commented out
+ # and leave 'force-reload' as an alias for 'restart'.
+ #
+ #log_daemon_msg "Reloading $DESC" "$NAME"
+ #do_reload
+ #log_end_msg $?
+ #;;
+ restart|force-reload)
+ #
+ # If the "reload" option is implemented then remove the
+ # 'force-reload' alias
+ #
+ log_daemon_msg "Restarting $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1)
+ do_start
+ case "$?" in
+ 0) log_end_msg 0 ;;
+ 1) log_end_msg 1 ;; # Old process is still running
+ *) log_end_msg 1 ;; # Failed to start
+ esac
+ ;;
+ *)
+ # Failed to stop
+ log_end_msg 1
+ ;;
+ esac
+ ;;
+ *)
+ #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
+ echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+ exit 3
+ ;;
+esac
+
+:
diff --git a/extra/outbound.service b/extra/outbound.service
new file mode 100644
index 0000000..a33a1f3
--- /dev/null
+++ b/extra/outbound.service
@@ -0,0 +1,21 @@
+[Unit]
+Description=outbound link usage tracker webapp
+Wants=network-online.target
+After=network.target syslog.target
+
+[Service]
+Environment=OUTBOUND_WSGI_INI=/etc/outbound.wsgi.ini
+Environment=OUTBOUND_CONF=/etc/outbound.conf
+WorkingDirectory=/var/outbound
+User=outbound
+Group=outbound
+Type=simple
+ExecStart=/usr/sbin/outbound.bin
+TimeoutStartSec=120
+ExecStop=/usr/sbin/uwsgi --stop /var/outbound/outbound-wsgi.pid
+RestartSec=15
+Restart=always
+KillSignal=SIGINT
+
+[Install]
+WantedBy=multi-user.target
diff --git a/extra/outbound.spec b/extra/outbound.spec
new file mode 100644
index 0000000..3ad3aaa
--- /dev/null
+++ b/extra/outbound.spec
@@ -0,0 +1,150 @@
+# File: outbound.spec
+# Location: outbound package
+# Author: bgstack15
+# SPDX-License-Identifier: CC-BY-SA-4.0
+# Startdate: 2022-11-29
+# Title: Rpm spec for outbound package
+# Purpose: Provide build instructions for CentOS rpm for package
+# History:
+# Usage:
+# Reference:
+# fifconfig.spec
+# Improve:
+# Documentation:
+# Dependencies:
+
+# Tunables
+# If you set this to 1, use the systemd-rpm-macros functionality described at https://docs.fedoraproject.org/en-US/packaging-guidelines/UsersAndGroups/
+%global with_systemd_usercreate 0
+%global pythonver python36
+
+# Fedora defaults
+%if 0%{?fedora}
+%global with_systemd_usercreate 1
+%global pythonver python3
+%endif
+
+%global _appvardir %{?_localstatedir}%{!?_localstatedir:/var}/outbound
+%global _user outbound
+
+%define devtty "/dev/null"
+%define debug_package %{nil}
+%global _python_bytecompile_errors_terminate_build 0
+
+Summary: Outbound link usage tracker
+Name: outbound
+Version: 0.0.1
+Release: 1
+License: GPL 3.0
+Source0: https://bgstack15.ddns.net/cgit/%{name}/snapshot/%{name}-master.tar.gz
+%if 0%{?with_systemd_usercreate}
+Source1: extra/%{name}.sysusers
+%endif
+#%%if ! 0%%{?fedora}
+#Patch1: extra/%%{name}-el7.patch
+#%%endif
+URL: https://bgstack15.ddns.net/
+Packager: B. Stack <bgstack15@gmail.com>
+Requires: %{pythonver}-flask
+Requires: %{pythonver}-xmltodict
+Requires: uwsgi-plugin-%{pythonver}
+Requires: uwsgi-logger-file
+%if 0%{?fedora}
+BuildRequires: make
+# Fedora needs pip helper here
+#Requires: %{pythonver}-json2html
+%endif
+# Mandatory pip3 requirements: pytimeparse, flask-sqlalchemy
+#BuildRequires: txt2man
+%if 0%{?with_systemd_usercreate}
+BuildRequires: systemd-rpm-macros
+%endif
+%if 0%{?fedora} || 0%{?rhel} >= 8
+Suggests: nginx
+%endif
+Buildarch: noarch
+
+%description
+Stackbin is a flask-based pastebin implementation.
+
+%prep
+%setup -q -c %{name}
+test -d "%{name}" && cd "%{name}" ; test -d "%{name}-"* && cd "%{name}-"* ;
+#%%if ! 0%%{?fedora}
+#%%patch1 -p1
+#%%endif
+
+%build
+export srcdir="extra"
+test -d "%{name}" && cd "%{name}" ; test -d "%{name}-"* && cd "%{name}-"* ;
+%make_build -C "${srcdir}"
+
+%install
+export srcdir="extra"
+test -d "%{name}" && cd "%{name}" ; test -d "%{name}-"* && cd "%{name}-"* ;
+%make_install -C "${srcdir}" \
+ with_systemd=YES \
+ with_nginx=YES \
+ with_apache=NO \
+ with_init=NO \
+%if 0%{?fedora}
+ with_pip_helper=NO
+%else
+ with_pip_helper=YES
+%endif
+%if 0%{?with_systemd_usercreate}
+install -p -D -m 0644 %{SOURCE1} %{buildroot}%{_sysusersdir}/%{name}.conf
+%endif
+exit 0
+
+%clean
+rm -rf %{buildroot}
+
+%pre
+# Reference: squid.spec
+%if 0%{?with_systemd_usercreate}
+%sysusers_create_compat %{SOURCE1}
+%else
+if ! getent group %{_user} 1>/dev/null 2>&1 ;
+then
+ /usr/sbin/groupadd --system --gid 726 %{_user}
+fi
+if ! getent passwd %{_user} 1>/dev/null 2>&1 ;
+then
+ /usr/sbin/useradd --system --gid $( /usr/bin/getent group ${_user} | /usr/bin/awk -F':' '{print $3}' ) \
+ --uid 726 --comment "outbound link tracker system user" \
+ --home-dir %{_appvardir} --shell /sbin/nologin \
+ %{_user}
+fi
+%endif
+exit 0
+
+%preun
+%systemd_postun_with_restart %{name}.service
+
+%post
+%systemd_post %{name}.service
+
+%postun
+%systemd_postun_with_restart %{name}.service
+
+%files
+%if 0%{?with_systemd_usercreate}
+%{_sysusersdir}/%{name}.conf
+%endif
+%attr(0640, %{_user}, %{_user}) %config(noreplace) %{_sysconfdir}/%{name}.conf
+%attr(0640, %{_user}, %{_user}) %config(noreplace) %{_sysconfdir}/%{name}.wsgi.ini
+%attr(0640, %{_user}, %{_user}) %{_libexecdir}/%{name}/*.py
+%attr(0755, %{_user}, %{_user}) %{_sbindir}/%{name}.bin
+%attr(0750, %{_user}, %{_user}) %dir %{_appvardir}
+%if ! 0%{?fedora}
+%attr(0755, -, -) %{_libexecdir}/%{name}/pip-helper.sh
+%endif
+%attr(0644, root, root) %{?_unitdir}%{!?_unitdir:/usr/lib/systemd/system}/%{name}.service
+%attr(0644, root, root) %{_sysconfdir}/nginx/default.d/%{name}.conf
+%attr(0750, %{_user}, %{_user}) %dir %{?_localstatedir}%{!?_localstatedir:/var}/log/%{name}
+%{_defaultdocdir}/%{name}
+
+%changelog
+* Tue Feb 15 2022 B. Stack <bgstack15@gmail.com> - 0.0.1-1
+- Initial release
diff --git a/extra/outbound.sysusers b/extra/outbound.sysusers
new file mode 100644
index 0000000..813b698
--- /dev/null
+++ b/extra/outbound.sysusers
@@ -0,0 +1,3 @@
+# Part of fifconfig package
+#Type Name ID GECOS Home directory Shell
+u fifconfig - "fifconfig system user" /var/fifconfig /sbin/nologin
diff --git a/extra/pip-helper.sh b/extra/pip-helper.sh
new file mode 100644
index 0000000..e2dd700
--- /dev/null
+++ b/extra/pip-helper.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+# This file is part of the outbound package.
+# It is designed to be run as root, and it will run the requisite pip commands for the discovered environment.
+
+contents="$( awk -F'=' '/ID=|VERSION_ID/{print $2}' /etc/os-release 2>/dev/null | sed -r -e 's/"//g;' )"
+id="$( echo "${contents}" | sed -n -e '1p' )"
+version_id="$( echo "${contents}" | sed -n -e '2p' )"
+
+echo "Any parameters sent to this script ${0} will be added to the list of packages to install."
+piplist="${1}"
+
+if echo "${id}" | grep -qiE 'rhel|centos' ;
+then
+ #if echo "${version_id}" | grep -qiE '7' ;
+ #then
+ piplist="${piplist} "
+ #fi
+elif echo "${id}" | grep -qiE 'fedora' ;
+then
+ piplist="${piplist} "
+elif echo "${id}" | grep -qiE 'devuan|debian' ;
+then
+ piplist="${piplist} "
+else
+ echo "Unknown os from /etc/os-release. Please investigate what pip3" 1>&2
+ echo "packages are required for this OS release and share it with upstream." 1>&2
+ echo "Aborted." 1>&2
+ exit 1
+fi
+
+if test -n "${piplist}" ;
+then
+ echo "Will try to serially install with pip these packages: ${piplist}"
+ for word in ${piplist} ; do
+ su outbound -s /bin/sh -c "pip3 install --user ${word}" || exit 1
+ done
+fi
diff --git a/outbound.bin b/outbound.bin
new file mode 100755
index 0000000..0e9d18a
--- /dev/null
+++ b/outbound.bin
@@ -0,0 +1,11 @@
+#!/bin/sh
+# Reference: fifconfig.bin from stackbin project
+# Startdate: 2022-11-29
+test -z "${OUTBOUND_WSGI_INI}" && {
+ thisscript="$( readlink -f "${0}" )"
+ OUTBOUND_WSGI_INI="$( dirname "${thisscript}" )/$( basename "${thisscript}" | sed -r -e 's/\.bin$//;' ).wsgi.ini"
+}
+COMMAND=""
+grep -qiE 'ID=.*(rhel|centos|fedora)' /etc/os-release && COMMAND="${COMMAND} uwsgi" || \
+ COMMAND="${COMMAND:+${COMMAND} }uwsgi_python39"
+${COMMAND} --ini "${OUTBOUND_WSGI_INI}"
diff --git a/outbound.conf.example b/outbound.conf.example
new file mode 100644
index 0000000..cdb5635
--- /dev/null
+++ b/outbound.conf.example
@@ -0,0 +1,18 @@
+# File: outbound.conf.example
+
+# WHITELIST of domains, one of which must be in the Referer header, to be given a redirect. If the referer is absent or not in this list, then the requester gets response 404.
+WHITELIST = [
+ "d2-03a",
+ "d2-03a:5000",
+ "d2-03a.ipa.internal.com",
+ "localhost",
+ "www.internal.com",
+ "www.example.com",
+ "server3",
+ "server3.ipa.internal.com",
+ "ipa.internal.com"
+]
+# DEBUG shows a small amount of debug info in server logs.
+DEBUG = False
+# ALLOW_DEBUG allows showing info to the requester if the requester provides header 'Debug: true'. It is recommended to disable this for production.
+ALLOW_DEBUG = False
diff --git a/outbound.py b/outbound.py
new file mode 100644
index 0000000..34f114e
--- /dev/null
+++ b/outbound.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# File: outbound.py
+# Location: https://bgstack15.ddns.net/cgit/outbound/
+# Author: bgstack15
+# Startdate: 2022-11-28-2 16:34
+# SPDX-License-Identifier: GPL-3.0
+# Title: Outbound flask web app
+# Project: outbound
+# Purpose: allow web server to log outbound links, by using a link underneath this webapp entry point
+# History:
+# Usage: See README
+# References:
+# fuss.py, coupons.py
+# https://werkzeug.palletsprojects.com/en/2.2.x/serving/#werkzeug.serving.run_simple
+# Improve:
+# ensure it works under an arbitrary top-level path like "/outbound/"
+# add domain and port globbing in WHITELIST
+# Dependencies:
+# python3-flask
+# Documentation: See README
+
+from flask import Flask, request, redirect, abort
+from urllib.parse import urlparse
+import sys, os, re
+# Load app from same directory as this file.py
+#sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+from urllib import parse
+
+app = Flask(__name__)
+try:
+ app.config.from_pyfile(os.environ["OUTBOUND_CONF"], silent=False)
+except:
+ try:
+ app.config.from_pyfile("/etc/outbound.conf")
+ except:
+ try:
+ # from working directory
+ app.config.from_pyfile("outbound.conf")
+ except:
+ try:
+ # from same directory
+ app.config.from_pyfile(os.path.join(os.path.dirname(os.path.realpath(__file__)),"outbound.conf"))
+ except:
+ # defaults
+ print(f"WARNING: Using default configs because could not find OUTBOUND_CONF, or /etc/outbound.conf, or ./outbound.conf!")
+ app.config.update(
+ WHITELIST = ["localhost"],
+ DEBUG = False,
+ ALLOW_DEBUG = False
+ )
+if "WSGI_LOGGING" in app.config:
+ dictConfig(app.config["WSGI_LOGGING"])
+
+# satisfies mod_wsgi:
+application = app
+
+@app.route("/<path:url>", methods=["GET"])
+def root(url = ""):
+ referer = None
+ allow_debug = app.config["ALLOW_DEBUG"]
+ _debug = app.config["DEBUG"]
+ try:
+ #print(dict(request.headers))
+ for item in dict(request.headers):
+ #print(f"Item: {item}, {request.headers.get('item')}")
+ #print(f"Evaluating item {item}")
+ if item.lower() in ["debug"]:
+ #print(f"DEBUG header: {request.headers.get(item).lower()}")
+ if request.headers.get(item).lower() in ["true","yes","on","1"]:
+ #print(f"{item}: {request.headers.get(item)}")
+ if allow_debug:
+ _debug = True
+ except:
+ pass
+ bad_referer = False
+ try:
+ referer = request.referrer
+ domain = urlparse(referer).netloc
+ if _debug:
+ print(f"Referer: {referer}, domain {domain}")
+ # TODO: allow wildcard/globbing
+ if domain in app.config["WHITELIST"]:
+ # Main logic to redirect to/print url.
+ # need to fix the webserver->flask collapse of the parameter string http:// down to http:/
+ new_url = re.sub("(((ht|f)tps?|gemini):)\/+","\\1//",url)
+ if _debug and allow_debug:
+ return f"redirect to {new_url}\n",307
+ else:
+ return redirect(new_url, 307)
+ else:
+ #print("Bad referer 1")
+ bad_referer = True
+ except:
+ #print("Bad referer 2")
+ bad_referer = True
+ if bad_referer:
+ if _debug and allow_debug:
+ return f"<html>Invalid referer: {referer}</html>\n",404
+ else:
+ abort(404)
+ return "OK", 200
+
+if __name__ == "__main__":
+ app.run()
diff --git a/outbound.wsgi.ini.dev b/outbound.wsgi.ini.dev
new file mode 100644
index 0000000..1825540
--- /dev/null
+++ b/outbound.wsgi.ini.dev
@@ -0,0 +1,17 @@
+[uwsgi]
+# CentOS 7 uwsgi needs "python36" added to this list.
+#plugins = logfile, python36
+# Devuan Ceres does not.
+plugins = logfile
+http-socket = 0.0.0.0:4682
+wsgi-file = outbound.py
+callable = app
+touch-reload = outbound.py
+touch-reload = outbound.conf
+touch-reload = outbound.wsgi.ini
+req-logger = file:log/req.log
+# to get strftime format fields, you need double percent signs
+logdate = "%%FT%%T"
+logger = file:log/outbound.log
+# the init script uses a different pidfile owned by root.
+pidfile = var/outbound-wsgi.pid
diff --git a/outbound.wsgi.ini.example b/outbound.wsgi.ini.example
new file mode 100644
index 0000000..f6f898a
--- /dev/null
+++ b/outbound.wsgi.ini.example
@@ -0,0 +1,17 @@
+[uwsgi]
+# CentOS 7 uwsgi needs "python36" added to this list.
+plugins = logfile, python36
+# Devuan Ceres does not.
+#plugins = logfile
+http-socket = 127.0.0.1:4682
+wsgi-file = /usr/libexec/outbound/outbound.py
+callable = app
+touch-reload = /usr/libexec/outbound/outbound.py
+touch-reload = /etc/outbound.conf
+touch-reload = /etc/outbound.wsgi.ini
+req-logger = file:/var/log/outbound/req.log
+# to get strftime format fields, you need double percent signs
+logdate = "%%FT%%T"
+logger = file:/var/log/outbound/outbound.log
+# the init script uses a different pidfile owned by root.
+pidfile = /var/outbound/outbound-wsgi.pid
bgstack15