#!/bin/bash
###############################################################################
#                                                                             #
# IPFire.org - A linux based firewall                                         #
# Copyright (C) 2012  IPFire Network Development Team                         #
#                                                                             #
# This program is free software: you can redistribute it and/or modify        #
# it under the terms of the GNU General Public License as published by        #
# the Free Software Foundation, either version 3 of the License, or           #
# (at your option) any later version.                                         #
#                                                                             #
# This program is distributed in the hope that it will be useful,             #
# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
# GNU General Public License for more details.                                #
#                                                                             #
# You should have received a copy of the GNU General Public License           #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
#                                                                             #
###############################################################################

. /usr/lib/network/header-zone

HOOK_SETTINGS="HOOK ACCESS_CONCENTRATOR AUTH USERNAME PASSWORD"
HOOK_SETTINGS="${HOOK_SETTINGS} SERVICE_NAME MTU IPV6 PREFIX_DELEGATION"

# User credentials for the dialin.
USERNAME=""
PASSWORD=""

# Set the authentication mechanism.
AUTH=

# Access Concentrator.
ACCESS_CONCENTRATOR=""

# Service name.
SERVICE_NAME=""

# Maximum Transmission Unit.
MTU=

# This hook can work with all authentication methods supported by pppd.
PPPOE_SUPPORTED_AUTH_METHODS="${PPP_SUPPORTED_AUTH_METHODS}"
PPPOE_PLUGIN="rp-pppoe.so"

# Request an IPv6 address.
IPV6="true"

# Use IPv6 prefix delegation.
PREFIX_DELEGATION="true"

hook_check_settings() {
	assert isset USERNAME
	assert isset PASSWORD

	isset AUTH && assert isoneof AUTH ${PPPOE_SUPPORTED_AUTH_METHODS}

	assert isset IPV6
	assert isset PREFIX_DELEGATION
}

hook_parse_cmdline() {
	while [ $# -gt 0 ]; do
		case "${1}" in
			--access-concentrator=*)
				ACCESS_CONCENTRATOR=$(cli_get_val "${1}")
				;;
			--auth=*)
				AUTH=$(cli_get_val "${1}")
				;;
			--ipv6=*)
				local value="$(cli_get_val "${1}")"
				if enabled value; then
					IPV6="true"
				else
					IPV6="false"
				fi
				;;
			--mtu=*)
				MTU=$(cli_get_val "${1}")
				;;
			--password=*)
				PASSWORD=$(cli_get_val "${1}")
				;;
			--prefix-delegation=*)
				PREFIX_DELEGATION="$(cli_get_bool "${1}")"
				;;
			--service-name=*)
				SERVICE_NAME=$(cli_get_val "${1}")
				;;
			--username=*)
				USERNAME=$(cli_get_val "${1}")
				;;
			*)
				warning "Unknown argument: ${1}" >&2
				;;
		esac
		shift
	done
}

hook_up() {
	local zone=${1}
	assert isset zone

	# If this zone's port is not set, we will return
	# with EXIT_OK so that this zone will remain active,
	# but we cannot start pppd.
	local port=$(__hook_get_port "${zone}")
	if ! isset port || ! port_exists "${port}"; then
		log WARNING "Could not bring up zone '${zone}' because no port is attached"
		exit ${EXIT_OK}
	fi

	zone_settings_read "${zone}"

	# Load the pppoe kernel module
	module_load "pppoe"

	# Bring up the port.
	port_up "${port}"

	# Start the ppp daemon.
	pppd_start ${zone}

	exit ${EXIT_OK}
}

hook_down() {
	local zone=${1}
	assert isset zone

	zone_settings_read "${zone}"

	# Stop the ppp daemon.
	pppd_stop ${zone}

	# Bring down the port.
	local port=$(__hook_get_port "${zone}")
	if isset port; then
		log DEBUG "Bringing down port '${port}'"
		port_down "${port}"
	fi

	exit ${EXIT_OK}
}

hook_hotplug() {
	local zone="${1}"

	case "$(hotplug_action)" in
		add)
			if hotplug_event_interface_is_port_of_zone "${zone}"; then
				# Bring up the zone if it is enabled but not active, yet.
				zone_start_auto "${zone}"

				exit ${EXIT_OK}
			fi
			;;
		remove)
			# PPPoE cannot work if the ethernet device has been removed
			if hotplug_event_interface_is_port_of_zone "${zone}"; then
				if zone_is_active "${zone}"; then
					zone_stop "${zone}"
				fi

				exit ${EXIT_OK}
			fi
			;;
	esac

	exit ${EXIT_NOT_HANDLED}
}

hook_discover() {
	local device=${1}

	# This obviously only works on ethernet (or compatible) devices
	if ! device_is_ethernet_compatible "${device}"; then
		exit ${EXIT_ERROR}
	fi

	local output
	output=$(pppoe-discovery -I ${device} -U $(uuid) 2>&1)

	# Exit if there was not output
	[ -z "${output}" ] && exit ${DISCOVER_ERROR}

	# Exit if PADI timed out
	grep -q "Timeout" <<<${output} && exit ${DISCOVER_ERROR}

	local ac
	while read line; do
		case "${line}" in
			Access-Concentrator:*)
				ac="${line#Access-Concentrator: }"
				;;
		esac
	done <<<"${output}"

	echo "ACCESS_CONCENTRATOR=\"$ac\""

	exit ${DISCOVER_OK}
}

hook_status() {
	local zone=${1}
	assert isset zone

	cli_device_headline ${zone}

	zone_settings_read "${zone}"

	cli_headline 2 "Configuration"
	cli_print_fmt1 2 "Username" "${USERNAME}"
	cli_print_fmt1 2 "Password" "<hidden>"

	local port=$(__hook_get_port "${zone}")
	if isset port; then
		cli_print_fmt1 2 "Port" "${port}"
	fi
	cli_space

	# Exit if zone is down
	if ! zone_is_up ${zone}; then
		echo # Empty line
		exit ${EXIT_ERROR}
	fi

	# XXX display time since connection started

	cli_headline 2 "Point-to-Point-over-Ethernet protocol"
	cli_print_fmt1 2 "MAC-Remote"  "$(db_get "${zone}/remote-address")"
	cli_space

	local proto
	for proto in ${IP_SUPPORTED_PROTOCOLS}; do
		db_exists "${zone}/${proto}" || continue

		local headline
		case "${proto}" in
			ipv6)
				headline="Internet Protocol Version 6"
				;;
			ipv4)
				headline="Internet Protocol Version 4"
				;;
			*)
				headline="Unkown protocol"
				;;
		esac
		cli_headline 3 "${headline}"

		cli_print_fmt1 3 "IP address"  "$(db_get "${zone}/${proto}/local-ip-address")"
		cli_print_fmt1 3 "Gateway"     "$(db_get "${zone}/${proto}/remote-ip-address")"
		cli_print_fmt1 3 "DNS servers" "$(db_get "${zone}/${proto}/domain-name-servers")"
		cli_space
	done

	exit ${EXIT_OK}
}

hook_ppp_write_config() {
	local zone=${1}
	assert isset zone

	local file=${2}
	assert isset file

	# Read in the configuration files.
	zone_settings_read "${zone}"

	# A port has to be assigned for this action
	local port=$(__hook_get_port "${zone}")
	if ! isset port; then
		error "No port assigned to pppoe hook of zone '${zone}'"
		exit ${EXIT_ERROR}
	fi

	# Prepare the command line options for the pppoe plugin.
	local plugin_options

	# Add the access concentrator (if any).
	if isset ACCESS_CONCENTRATOR; then
		plugin_options="${plugin_options} rp_pppoe_ac '${ACCESS_CONCENTRATOR}'"
	fi

	# Add the service name (if any).
	if isset SERVICE_NAME; then
		plugin_options="${plugin_options} rp_pppoe_service '${SERVICE_NAME}'"
	fi

	# The last argument must be the interface.
	plugin_options="${plugin_options} ${port}"

	pppd_write_config ${file} \
		--interface="${zone}" \
		--username="${USERNAME}" \
		--password="${PASSWORD}" \
		--mtu="${MTU}" \
		--auth="${AUTH}" \
		--ipv6="${IPV6}" \
		\
		--plugin="${PPPOE_PLUGIN}" \
		--plugin-options="${plugin_options}"

	exit ${EXIT_OK}
}

__hook_get_port() {
	local zone="${1}"

	local port
	for port in $(zone_get_ports "${zone}"); do
		echo "${port}"
		return ${EXIT_OK}
	done

	return ${EXIT_ERROR}
}

hook_port_attach() {
	# Excepting at least two arguments here
	assert [ $# -ge 2 ]

	local zone="${1}"
	local port="${2}"
	shift 2

	# PPPoE can only use one port
	local ports_num="$(zone_get_ports_num "${zone}")"
	if [ ${ports_num} -ge 1 ]; then
		local ports="$(zone_get_ports "${zone}")"
		error "The pppoe zone hook only supports assigning one port"
		error "  port '${ports}' has already been assigned to zone '${zone}'"
		return ${EXIT_ERROR}
	fi

	if ! zone_port_settings_write "${zone}" "${port}"; then
		exit ${EXIT_ERROR}
	fi

	exit ${EXIT_OK}
}

hook_port_detach() {
	assert [ $# -eq 2 ]

	local zone="${1}"
	local port="${2}"

	# Shut down the entire zone here, because it cannot
	# run without a port any way and removing the port would
	# create a hotplug event which will be processed after the
	# port has already been detached...
	zone_stop "${zone}"

	if ! zone_port_settings_remove "${zone}" "${port}"; then
		exit ${EXIT_ERROR}
	fi

	exit ${EXIT_OK}
}

hook_port_up() {
	assert [ $# -eq 2 ]

	local zone="${1}"
	local port="${2}"

	# Try bringing up the port if it has not been brought up before
	if ! device_exists "${port}"; then
		port_create "${port}"
	fi

	# Make sure that the port is up
	port_up "${port}"

	exit ${EXIT_OK}
}

hook_port_down() {
	assert [ $# -eq 2 ]

	local zone="${1}"
	local port="${2}"

	if device_exists "${port}"; then
		port_down "${port}"
	fi

	exit ${EXIT_OK}
}

hook_ppp_ipv6_up() {
	local zone="${1}"

	ppp_common_ipv6_up "${zone}"

	# Read configuration
	zone_settings_read "${zone}"

	if enabled PREFIX_DELEGATION; then
		dhclient_start "${zone}" ipv6
	fi

	exit ${EXIT_OK}
}

hook_ppp_ipv6_down() {
	local zone="${1}"

	ppp_common_ipv6_down "${zone}"

	# Read configuration
	zone_settings_read "${zone}"

	if enabled PREFIX_DELEGATION; then
		dhclient_stop "${zone}" ipv6
	fi

	exit ${EXIT_OK}
}
