#!/usr/bin/env bash
# [ʞ] vpn
#  ~ lexi hale <lexi@hale.su>
#  ® affero general public license
#  $ vpn (join | part | info | clean | key) <name>
#  : vim:ft=bash

# vpn is a simple wrapper around openvpn to make it suitable
# for everyday  use. it  takes two  arguments: the  vpn, and
# what to do with it.
#   - vpn join <vpn>: opens a connection to vpn <name>
#   - vpn part <vpn>: closes an active connection to <name>
#   - vpn info <vpn>: reports the status of of a vpn
#   - vpn key <host>: automatically  provisions  the  client
#                     with RSA keys from  <host> and creates
#                     a  configuration file if one does  not
#                     already exist
#   - vpn part <vpn>: closes an active connection to <vpn>
#         - vpn help: display this text
#
# a number  of environment variables affect  the behavior of
# vpn. these  are listed in  the source with  their defaults
# and an  explantion of their  function. if you do  not wish
# users  to be  able  to  change the  behavior  of vpn  with
# setenv, you must  change the param invokation  to a simple
# assignment.
#
# note  that you  may want  to  add a  visudo line  allowing
# %wheel  or  perhaps  even  all users  to  execute  openvpn
# without a  password; otherwise, only sudoers  will be able
# to  use vpn  and the root password will be  required every
# time  a user  connects. root  is not  needed to  tear down
# connections, as by  default the openvpn process  is set to
# pivot to the  account that invoked the script  after it is
# done with tasks that require privileged access.
#
# (it should  go without  saying, but ensure  you understand
# the  security implications  before  editing  sudoers on  a
# multiuser machine or  one that is directly  exposed to the
# internet.)

_text_=$LINENO

param(){ eval $1=\${$1:-$2}; }

param vpn_basedir    ~/opt/vpn
	# the directory in which vpn's logfiles are stored, 
	# and possibly the script itself

param vpn_confdir    $vpn_basedir
	# the directory that contains the individual vpn
	# directories

param vpn_global     $vpn_basedir/conf
	# a configuration file that is applied to all vpns

param vpn_srv_keydir /srv/vpn/ca
	# the directory on the server where vpn client keys
	# are stored

param vpn_cn         $(uname -n)
	# the name of the device  / user / resource connecting.
	# `vpn  key` will  use  this value  to determine  which
	# client keys  to download. i recommend  a one-key-per-
	# device setup with the certificate CN used to assign a
	# name  in the  tunnel's DNS  server, but  one-key-per-
	# user or one-key-period setups are also possible.

param TMPDIR		 /tmp
	# a directory for temporary files, preferably one that
	# does not persist across boots (e.g. a tmpfs)

param USER			 $(whoami)
	# the user who should own all files and processes
	# created and destroyed by vpn

param vpn_pidbox     $TMPDIR/pid.$USER
	# a directory for storing pids in. this should be chmod
	# 700 and owned by the user invoking vpn, ideally

param vpn_bin        openvpn
	# the binary to use. if openvpn is not in your path,
	# enter its absolute path here

param vpn_script     $0
	# a path to the executable

param vpn_scrname    $(basename $vpn_script)
	# the name of the executable

# thus ends the admin-configurable portion of this script.
# abandon all hope, ye who enter here

err(){ echo -e "\e[1;31merror:\e[m" $* ; exit 1; }
assert(){ msg=$1; shift; test ! $@ && err $msg; }

test "$1" == help || assert "incorrect number of arguments" $# -eq 2

act=$1; target=$2
vpnd=$vpn_confdir/$target
conf=$vpnd/conf

test "$act" == key && {
	mkdir -p $vpnd
	test -e $conf || {
		echo dev tun > $conf
		echo proto udp >> $conf
		echo remote $target 1194 >> $conf
	}
}

assert "target \e[1m$target\e[m does not exist!" -e $vpn_confdir/$target
assert "target \e[1m$target\e[m has no configuration file" -e $conf

pidfile="$vpn_pidbox/openvpn.$target"

stale() {
	((clean == 1)) && {
		echo " → deleting stale pidfile"
		rm -f $pidfile
	} || {
		echo -e " - run \e[1m$vpn_scrname clean\e[m to clean up"
	}
}

goodpid(){
	test -e $pidfile || return 1
	local proc=$(ps ho fname $(cat $pidfile)) || return 2
	test "$proc" == openvpn || return 2
	return 0
}
	

case $act in
	# automatically provision machine
	( key | setup | provision | download | dl | k ) 
		scp $target:$vpn_srv_keydir/pki/{ca.crt,{issued,private}/$vpn_cn.\*} $vpnd/ || err "could not retrieve keys for \e[1m$target\e[m; ensure ssh is properly configured and machine keys are assigned to the correct user" ;;

	# connect to the openvpn server
	( join | up | connect | con | start | j | u )
		# don't clobber an existing connection
		goodpid && err "\e[1m$target\e[m is already up!"

		# make sure a private pid directory exists
		test ! -e $vpn_pidbox &&
			mkdir -p $vpn_pidbox
		chmod 700 $vpn_pidbox

		# check and see if we're using automatic
		# host certificates; tell openvpn if so
		test -e $vpnd/ca.crt && {
			hostcert=$vpnd/$vpn_cn
			cmd=(
				--askpass
				--ca   $vpnd/ca.crt
				--cert $hostcert.crt
				--key  $hostcert.key
			)
		}

		touch $pidfile # make sure invoker owns it
		chmod 600 $pidfile # bc fuck you, that's why
		sudo openvpn --daemon --config $vpn_global \
		                      --config $conf       \
							  --user $USER         \
							  ${cmd[@]}            \
		                      --writepid $pidfile || rm $pidfile;;

	# kill an existing connection
	( part | down | disc* | stop | p | d )
		test -e $pidfile && {
			goodpid && {
				kill $(cat $pidfile) && rm -f $pidfile
			} || {
				echo -e "\e[1;33mwarn:\e[m pidfile exists but does not name an openvpn process"
				echo " → removing pidfile for safety"
				rm -f $pidfile
			}

		} || {
			echo -ne "\e[1mvpn $target\e[m is not up"
		} ;;

	# clean up dirty pidfiles
	( clean | wipe | clear | fix | tidy ) clean=1 ;&
	# return profile status
	( info | stat* | detail* | i )
		echo -ne "\e[1mvpn $target:\e[m "
		test ! -e $pidfile && {
			echo -ne "\e[31mno\e[m current connection"
			pids=($(pidof openvpn))
			let pidc=${#pids}
			((pidc > 0)) && {
				echo ", but $pidc vpn instances are running"
				exit 2
			} || { echo; exit 0; }
		} || {
			echo -n "pidfile exists"
			proc=$(ps ho fname $(cat $pidfile))
			(($? != 0)) && {
				echo ", but there are no processes with that pid"
				stale; exit 1
			} || test "$proc" == "openvpn" && {
				echo -e " and openvpn is \e[32mrunning\e[m"
				exit 0
			} || {
				echo -e " but named process is \e[31mnot a vpn instance!\e[m"
				stale; exit 1
			}
		} ;;
	( help ) head -n $(expr $_text_ - 2) $vpn_script |
		tail -n $(expr $_text_ - 2); exit 255;;

	( * ) err "action must be one of: join | part | info | clean" ;;
esac

