#!/bin/sh

po_domain="alterator-openvpn-server"
alterator_api_version=1

. alterator-sh-functions
. shell-config
. cert-sh-functions
. alterator-net-functions

CHROOTDIR="/var/lib/openvpn"
CONFDIR="/etc/openvpn"
KEYSDIR="${SSL_KEYDIR:-$CONFDIR/keys}"
CERTSDIR="${SSL_CERTDIR:-$CONFDIR/keys}"
CSRDIR="${SSL_CSRDIR:-$CONFDIR/keys}"
ETCNET_IFACEDIR="/etc/net/ifaces"
OVPNCONFIG="ovpnoptions"
DEFAULT_SERVERNAME="openvpn-server"
DEFAULT_OWN_CA="$CERTSDIR/$DEFAULT_SERVERNAME-CA.crt"
DEFAULT_CA="$SSL_CERTDIR/root.pem"
DHPARAM_NUMBITS="1024"
CACHEDIR="/var/cache/alterator/openvpn-server"
CCDDIR="$CHROOTDIR/$CONFDIR/ccd"
IFACEDIR="$ETCNET_IFACEDIR"

### cache
init_cache()
{
	local dev="$1";shift

	[ -n "$dev" ] || return 1
	[ -d "$CACHEDIR" ] || mkdir "$CACHEDIR"
	if [ -d "$ETCNET_IFACEDIR/$dev" -a ! -d "$CACHEDIR/$dev" ];then
		cp -a "$ETCNET_IFACEDIR/$dev" "$CACHEDIR/$dev"
		IFACEDIR="$CACHEDIR"
	fi

	if [ -d "$CHROOTDIR/$CONFDIR/ccd" -a ! -d "$CACHEDIR/ccd" ];then
		cp -a "$CHROOTDIR/$CONFDIR/ccd" "$CACHEDIR/ccd"
		CCDDIR="$CACHEDIR/ccd"
	fi

}

clear_cache()
{
	IFACEDIR="$ETCNET_IFACEDIR"
	CCDDIR="$CHROOTDIR/$CONFDIR/ccd"
	rm -rf "$CACHEDIR"
}

commit_cache()
{
	local dev="$1";shift

	[ -n "$dev" ] || return 1

	if [ -d "$CACHEDIR/$dev" ];then
		rm -rf "$ETCNET_IFACEDIR/$dev"
		mv "$CACHEDIR/$dev" "$ETCNET_IFACEDIR/$dev"
	fi
	if [ -d "$CACHEDIR/ccd" ];then
		rm -rf "$CHROOTDIR/$CONFDIR/ccd"
		mv "$CACHEDIR/ccd" "$CHROOTDIR/$CONFDIR/ccd"
	fi

	clear_cache
}

get_cached_vdev()
{
	local tmp="$(find "$CACHEDIR" -type d \( -name tun\* -o -name tap\* \) | head -n1)"

	[ -n "$tmp" ] && echo "${tmp##*/}"
}
###

list_mask()
{
	for i in `seq 32 -1 0`; do
		echo "$(maskname "$i")"
	done
}

list_types()
{
	write_enum_item "routed" "`_ "Routed (tun)"`"
	write_enum_item "bridged" "`_ "Bridged (tap)"`"
}

list_clients()
{
	ls "$CCDDIR"
}

list_client_network()
{
	local client_name="$1";shift
	local client_file="$CCDDIR/$client_name"

	[ -n "$client_name" -a -f "$client_file" ] &&
	shell_config_get "$client_file" iroute ' ' | tr -s ' ' '/'
}

push_route()
{
	local dev="$1";shift

	echo "push \"route $1 $2\"" >>$CACHEDIR/$dev/$OVPNCONFIG
}

make_iface_options()
{
	local options="$1/options";shift

	cat > "$options" <<OPTIONS_TEMPLATE
ONBOOT=yes
TYPE=ovpn
BOOTPROTO=static
OPTIONS_TEMPLATE
}

add_route()
{
	local dev="$1"; shift
	local client_net="$1"; shift
	local client_netmask="$1"; shift

	echo "route $client_net $client_netmask" >>$CACHEDIR/$dev/$OVPNCONFIG
	push_route "$dev" "$client_net" "$client_netmask"
}

add_client_iroute()
{
	local ccd_dir="$CACHEDIR/ccd"
	local client_name="$1"; shift
	local client_net="$1"; shift
	local client_netmask="$1"; shift

	[ -d "$ccd_dir" ] || mkdir "$ccd_dir"
	[ -n "$client_name" -a -n "$client_net" -a -n "$client_netmask" ] && 
		echo "iroute $client_net $client_netmask" >>$ccd_dir/$client_name
}

remove_client_iroute()
{
	local client_name="$1";shift
	local client_net="$1";shift
	local client_netmask="$1";shift
	local client_file="$CACHEDIR/ccd/$client_name"
	local tmp="$(mktemp -t "$client_name".XXXXXX)"

	[ -n "$client_name" -a -n "$client_net" -a -n "$client_netmask" -a -f "$client_file" ] &&
	sed -r /"$client_net"[[:space:]]+"$client_netmask"/d "$client_file" >"$tmp" &&
	mv -f "$tmp" "$client_file"
	[ -s "$client_file" ] || rm -f "$client_file"
}

add_all_clients_routes()
{
	local ccd_dir="$CACHEDIR/ccd"
	local dev="$1";shift
	local network=
	for i in $(list_clients)
	do
		#Spaces substituted on '|' for for easier splitting of strings
		networks="$(shell_config_get "$ccd_dir/$i" iroute ' ' | sed 's/[[:space:]]\+/|/')"
		for i in $networks;do
			add_route "$dev" "${i%|*}" "${i#*|}"
		done	
	done
}

push_ns()
{
	echo "push \"dhcp-option DNS $1\"" >>$CACHEDIR/$dev/$OVPNCONFIG
}

make_server_conf()
{
	local dev="$1";shift
	local server_param=
	local servername="$DEFAULT_SERVERNAME"
	local ca="$DEFAULT_CA"
    local ns=

	[ -d "$CACHEDIR/$dev" ] || { mkdir "$CACHEDIR/$dev" || return 1; }
	[ ! -f "$CACHEDIR/$dev/options" ] && { make_iface_options "$CACHEDIR/$dev" || return 1; }

	if [ "$in_type" = 'routed' ];then
		server_param="server $in_vpnnet $in_vpnnetmask"
	elif [ "$in_type" = 'bridged' ];then
		server_param="server-bridge $in_server_vpnaddr $in_vpnnetmask $in_vpnpool_start $in_vpnpool_end"
	fi

	[ -s "$DEFAULT_OWN_CA" ] && ca="$DEFAULT_OWN_CA"

        cat > "$CACHEDIR/$dev/$OVPNCONFIG" <<SERVER_CONF_TEMPLATE
port $in_port
proto udp
dev  $dev
ca   $ca
cert $CERTSDIR/$servername.cert
key  $KEYSDIR/$servername.key
dh   $KEYSDIR/$servername.dh
$server_param
user openvpn
group openvpn
ifconfig-pool-persist ipp.txt
keepalive 10 120
comp-lzo
client-config-dir $CONFDIR/ccd
persist-key
persist-tun
client-to-client
status openvpn-status.log
verb 3
SERVER_CONF_TEMPLATE
if [ "$in_type" = 'routed' ];then
	push_route "$dev" "$in_server_net" "$in_server_netmask"
	add_all_clients_routes $dev
fi
ns="$(get_ns "$in_server_net" "$in_server_netmask")"
[ -n "$ns" ] && push_ns "$ns"
}

get_config_val()
{
	local dev="$1";shift
	local name="$1";shift
	echo $(shell_config_get "$IFACEDIR/$dev/$OVPNCONFIG" "$name" ' ') | sed 's/[[:space:]]*#.*$//'
}

read_server_net()
{
	local dev="$1";shift

	sed -r -n 's/^[[:space:]]*push[[:space:]]+"route[[:space:]]+([[:digit:].]+)[[:space:]]+([[:digit:].]+)"/\1 \2/p' \
                -- "$IFACEDIR/$dev/$OVPNCONFIG" | head -n1
}

read_bridged_vpn_params()
{
	local dev="$1";shift

	get_config_val "$dev" "server-bridge"
}

read_vpn_net()
{
	local dev="$1";shift

	get_config_val "$dev" "server"
}

get_addr()
{
	echo "$1" | cut -f1 -d ' '
}

get_netmask()
{
	echo "$1" | cut -f2 -d ' '
}

vdev_type()
{
	local type="$1"

	case "$type" in
		routed) echo 'tun'
		;;
		bridged) echo 'tap'
		;;
	esac
}

get_dev_type()
{
	case "$1" in
		tun*) echo 'tun'
		;;
		tap*) echo 'tap'
		;;
	esac
}

read_vpn_type()
{
	local dev="$1";shift

	if grep -qs '^server-bridge[[:space:]]' "$IFACEDIR/$dev/$OVPNCONFIG";then
		echo 'bridged'
	elif grep -qs '^server[[:space:]]' "$IFACEDIR/$dev/$OVPNCONFIG";then
		echo 'routed'
	fi
}

is_dev_type_changed()
{
	local dev="$1";shift
	local dev_type="$1";shift

	[ -n "$dev_type" ] && [ "$dev_type" != "$(get_dev_type "$dev")" ]
}

read_server_config()
{
	local dev="$1";shift
	local dev_conf=
	local dev_type="$(vdev_type "$in_type")"
	local server_network= vpn_network= bridged_vpn_params=
	local type="$in_type"

	if [ -n "$dev" ] &&	[ -f "$IFACEDIR/$dev/$OVPNCONFIG" ];then
		dev_conf="$(get_config_val "$dev" dev)"
		if [ "$dev" != "$dev_conf" ];then
			write_error "`_ "Invalid config value 'dev'"` $dev_conf"
			return
		fi
		write_bool_param enabled "$(shell_config_get "$IFACEDIR/$dev/options" 'ONBOOT')"
		[ -n "$type" ] || type="$(read_vpn_type "$dev")"
		write_string_param type "$type"
		write_string_param port "$(get_config_val "$dev" port)"
		server_network="$(read_server_net "$dev")"
		write_string_param server_net "$(get_addr "$server_network")"
		write_string_param server_netmask "$(get_netmask "$server_network")"
		if [ "$type" = 'routed' ];then
			vpn_network="$(read_vpn_net "$dev")"
			write_string_param vpnnet "$(get_addr "$vpn_network")"
			write_string_param vpnnetmask "$(get_netmask "$vpn_network")"
		else
			bridged_vpn_params="$(read_bridged_vpn_params "$dev")"
			write_string_param server_vpnaddr "$(get_addr "$bridged_vpn_params")"
			write_string_param vpnnetmask "$(get_netmask "$bridged_vpn_params")"
			write_string_param vpnpool_start "$(echo "$bridged_vpn_params" | cut -f3 -d' ')"
			write_string_param vpnpool_end "$(echo "$bridged_vpn_params" | cut -f4 -d' ')"
		fi
	else
		#default parameters
		write_bool_param enabled '#f'
		[ -z "$type" ] && type='routed'
		write_string_param type "$type"
		write_string_param port '1194'
		write_string_param server_netmask '255.255.255.0'
		write_string_param vpnnetmask '255.255.255.0'
		if [ "$type" = 'routed' ];then
			write_string_param vpnnet '10.8.0.0'
		else
			write_string_param server_vpnaddr "10.8.0.1"
			write_string_param vpnpool_start "10.8.0.2"
			write_string_param vpnpool_end "10.8.0.200"
		fi
	fi
}

read_client_ns_info()
{
	local dev="$1";shift
	local name="$1";shift
	local ns= domain=
	echo "dev=$dev"

	if [ -n "$dev" -a -f "$IFACEDIR/$dev/ifup-post" ];then
		echo "cat \"$IFACEDIR/$dev/ifup-post\" | sed -n \"s/^.*domain[[:space:]]\+\([[:alnum:].]\+\).*ovpn-$name$/\1/p\""
		domain="$(cat "$IFACEDIR/$dev/ifup-post" | sed -n "s/^.*domain[[:space:]]\+\([[:alnum:].]\+\).*ovpn-$name$/\1/p")"
		ns="$(cat "$IFACEDIR/$dev/ifup-post" | sed -n "s/^.*nameserver[[:space:]]\+\([[:alnum:].]\+\).*ovpn-$name$/\1/p")"
	fi
	echo "ns=$ns"
	echo "domain=$domain"
	write_string_param client_ns "$ns"
	write_string_param client_domain "$domain"
}

ip4addr_to_hex()
{
	if [ -n "$1" ];then
		local i=0
		local tmp="$(echo "$1" | tr '.' ' ')"
		for b in $tmp;do
			[ "$b" -ge 0 -a "$b" -le 255 ] || break
			i=$(($i+1))
		done
		[ "$i" -eq 4 ] && printf '0x%02x%02x%02x%02x' $tmp
	fi
}

is_ip4addr_in_subnet()
{
	local hex_addr="$(ip4addr_to_hex "$1")";shift
	local hex_net="$(ip4addr_to_hex "$1")";shift
	local prefix="$1";shift
	local p=0xffffffff
	local hex_mask="$(( $p - ($p >> $prefix) ))"
	[ "$(( $hex_net & $hex_mask ))" -eq "$(( $hex_addr & $hex_mask ))" ]
}

mask_to_prefix()
{
	local hex_mask="$(ip4addr_to_hex "$1")";shift

	if [ -n "$hex_mask" ];then
		local p=$(( ~$hex_mask & 0xffffffff ))
		local i=0 prefix=
		while [ "$p" -ne 0 ];do
			p=$(($p >> 1 & 0xffffffff))
			i=$(($i + 1))
		done
		prefix=$((32 - $i))
		[ "$prefix" -ge 0 -a "$prefix" -le 32 ] && echo "$prefix"
	fi
}

get_ip4_addr()
{
	local server_net="$1";shift
	local server_prefix="$1";shift

	for i in "$(iptables_helper show -i)";do
		local addr= prefix=
		local iface_net="$(ip addr show eth0 | sed -n 's;[[:space:]]\+inet[[:space:]]\+\([[:digit:]./]\+\).*;\1;p')"
		if [ -n "$iface_net" ];then
			addr="$(echo "$iface_net" | cut -f1 -d'/')"
			prefix="$(echo "$iface_net" | cut -f2 -d'/')"
			[ -n "$addr" -a -n "$prefix" -a -n "$server_net" -a -n "$server_prefix" -a \
					"$server_prefix" -eq "$prefix" ] && 
			is_ip4addr_in_subnet "$addr" "$server_net" "$prefix" && { echo "$addr"; break; }
		fi
	done
}

get_ns()
{
	local server_net="$1";shift
	local server_netmask="$1";shift
	local server_prefix="$(mask_to_prefix "$server_netmask")"

	for addr in $(shell_config_get '/etc/resolv.conf' 'nameserver' ' ');do
		if [ "$addr" = '127.0.0.1' ];then
			local ns="$(get_ip_addr "$server_net" "$server_prefix")"
			[ -n "$ns" ] && { echo "$ns"; break; }
		elif is_ip4addr_in_subnet "$addr" "$server_net" "$server_prefix";then 
			echo "$addr" 
			break
		fi
	done
}

find_server_vdev()
{
	local dev_type="$1";shift

	for i in `find $ETCNET_IFACEDIR -type d -name $dev_type\*`
	do
		[ -f $i/options ] &&
		grep -qs "^TYPE=ovpn" $i/options &&
		grep -qs "^server" $i/$OVPNCONFIG &&
		echo "${i##*/}" && break
	done
}

find_bridge_dev()
{
	for i in `find $ETCNET_IFACEDIR -type d -name br\*`
	do
		[ -f $i/options ] &&

		echo "${i##*/}" && break
	done

}

write_client_ns_info()
{
	local dev="$1";shift
	local name="$1";shift
	local domain="$1";shift
	local ns="$1";shift
	local line=

	if [ -z "$dev" ];then
		write_error "`_ "Device not specified"`"
		return 1
	fi
	[ -n "$name" -a -n "$domain" -a -n "$ns" ] || return 1
	[ -f "$CACHEDIR/$dev/ifup-post" ] || echo '#!/bin/sh' >"$CACHEDIR/$dev/ifup-post"
	sed -i "/resolvconf -a ovpn-$name/d" "$CACHEDIR/$dev/ifup-post"
	echo "echo -e 'domain $domain\\nnameserver $ns' | resolvconf -a ovpn-$name" >>"$CACHEDIR/$dev/ifup-post"
	chmod +x "$CACHEDIR/$dev/ifup-post"

	[ -f "$CACHEDIR/$dev/ifdown-post" ] || echo '#!/bin/sh' >"$CACHEDIR/$dev/ifdown-post"
	grep -qs "resolvconf -d ovpn-$name" "$CACHEDIR/$dev/ifdown-post" ||
		echo "resolvconf -d ovpn-$name" >>"$CACHEDIR/$dev/ifdown-post"
	chmod +x "$CACHEDIR/$dev/ifdown-post"
}

enable_vdev_forwarding()
{
	local dev_type="$1"

	[ -n "$dev_type" ] || return 1
	# Allow TUN/TAP interface connections to OpenVPN server
	iptables -A INPUT -i "$dev_type"+ -j ACCEPT

	# Allow TUN/TAP interface connections to be forwarded through other interfaces
	iptables -A FORWARD -i "$dev_type"+ -j ACCEPT
}

make_ssl_files()
{
	[ -n "$1" ] || return
	ssl_check_key "$1" || ssl_make_key "$1"
	ssl_check_req "$1" || ssl_make_req "$1" "/CN=$1"
	ssl_check_dhparam "$1" || ssl_make_dhparam "$1" "$DHPARAM_NUMBITS"
}

rm_ssl_files()
{
	[ -n "$1" ] || return
	rm -f "$KEYSDIR/$1.key"
	rm -f "$CERTSDIR/$1.cert"
	rm -f "$CSRDIR/$1.csr"
}

write_config()
{
	local dev="$1";shift
	local dev_type="$(vdev_type "$in_type")"

	[ -n "$dev" ] || dev="$(next_iface "$dev_type")"
	make_server_conf "$dev"
}

stop_server()
{
	local new_dev="$1"; shift
	local old_dev="$new_dev"
	local dev_type="$(vdev_type "$in_type")"
	if [ -z "$new_dev" ];then
		case "$dev_type" in
			tap)
			old_dev="$(find_server_vdev 'tun')"
			;;
			tun)
			old_dev="$(find_server_vdev 'tap')"
			;;
		esac
	fi

	if [ -n "$old_dev" ];then
		ifdown "$old_dev"
		is_dev_type_changed "$old_dev" "$dev_type" && rm -rf "$ETCNET_IFACEDIR/$old_dev"
	fi
}

stop_start_server()
{
	local dev="$1"; shift
	local servername="$DEFAULT_SERVERNAME"
	local dev_type="$(vdev_type "$in_type")"

	stop_server "$dev"
	commit_cache "$dev"
	make_ssl_files "$servername"

	if test_bool "$in_enabled";then
		shell_config_set "$ETCNET_IFACEDIR/$dev/options" 'ONBOOT' 'yes'
		if ! ssl_check_cert "$servername";then
			write_error "`_ "Certificate not found!"`"
		elif ! ssl_check_key "$servername";then
			write_error "`_ "Key not found!"`"
		elif [ ! -f "$DEFAULT_OWN_CA" -a ! -f "$DEFAULT_CA" ];then
			write_error "`_ "CA not found!"`"
		else
			enable_vdev_forwarding "$dev_type"
			ifup "$dev" || write_error "`_ "openvpn server start failed"`"
		fi
	else
		shell_config_set "$ETCNET_IFACEDIR/$dev/options" 'ONBOOT' 'no'
	fi
}

clear_cache

on_message()
{
  local dev_type="$(vdev_type "$in_type")"
  local vdev="$(find_server_vdev "$dev_type")"

  case "$in_action" in
	  read)
	  case "$in__objects" in
		  /)
		  if [ -n "$in_client_name" ];then
			  read_client_ns_info "$vdev" "$in_client_name"
		  else
			  read_server_config "$vdev"
		  fi
		  ;;
		  csr)
		  write_blob_param "csr" "$SSL_CSRDIR/$DEFAULT_SERVERNAME.csr"
		  ;;
	  esac
	  ;;
	write)
	init_cache "$vdev"
	case "$in_operation" in
		config)
		write_config "$vdev"
		;;
		apply)
		write_config "$vdev"
		stop_start_server "$vdev"
		;;
		reset)
		clear_cache
		;;
		add-client-network)
		[ -n "$vdev" ] || vdev="$(get_cached_vdev)"
		write_client_ns_info "$vdev" "$in_client_name" "$in_client_domain" "$in_client_ns"
		add_client_iroute "$in_client_name" "$in_client_net" "$in_client_netmask"
		;;
		remove-client-network)
		remove_client_iroute "$in_client_name" "$in_client_net" "$in_client_netmask"
		;;
		create-req)
		rm_ssl_files "$DEFAULT_SERVERNAME"
		make_ssl_files "$DEFAULT_SERVERNAME"
	esac
	;;
	list)
	case "${in__objects##*/}" in
		avail_masks) list_mask | write_enum
		;;
		avail_types) list_types
		;;
		avail_clients) list_clients | write_enum
		;;
		avail_clients_networks) 
		[ -n "$in_client_name" ] && list_client_network "$in_client_name"  | write_enum
		;;
	esac
	;;
	upload-ca)
	[ -n "$in_ca_cert" ] && echo "$in_ca_cert" >"$DEFAULT_OWN_CA"
	;;

	upload-cert)
	local tempfile="$(mktemp -t ovpn-cert.XXXXXX)"
	local certfile="$CERTSDIR/$DEFAULT_SERVERNAME.cert"
	local keyfile="$KEYSDIR/$DEFAULT_SERVERNAME.key"
	[ -n "$in_certificate" ] && echo "$in_certificate" >"$tempfile"

	#FIXME: need cert validation
	mv -f "$tempfile" "$certfile"
	;;

  esac
}

message_loop

