# Mgmt
# Copyright (C) James Shubin and the project contributors
# Written by James Shubin <james@shubin.ca> and the project contributors
#
# 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 <https://www.gnu.org/licenses/>.
#
# Additional permission under GNU GPL version 3 section 7
#
# If you modify this program, or any covered work, by linking or combining it
# with embedded mcl code and modules (and that the embedded mcl code and
# modules which link with this program, contain a copy of their source code in
# the authoritative form) containing parts covered by the terms of any other
# license, the licensors of this program grant you additional permission to
# convey the resulting work. Furthermore, the licensors of this program grant
# the original author, James Shubin, additional permission to update this
# additional permission if he deems it necessary to achieve the goals of this
# additional permission.

import "deploy"
import "fmt"
import "golang"
import "local"
import "golang/strings" as golang_strings


# Class prepare adds some common things you probably want to run when using this
# module.
class prepare() {
	sysctl "net.ipv4.ip_forward" { # firewalls love this!
		value => "1",
	}

	svc "firewalld" { # we don't want this
		state => "stopped",
		startup => "disabled",
	}
}

# XXX: The templates need a padding function to line up columns.
class firewall() {
	pkg "shorewall" {
		state => "installed",

		Before => File["/etc/shorewall/"],
	}

	file "/etc/shorewall/" {
		state => $const.res.file.state.exists,
		recurse => true,
		purge => true,
		owner => "root",
		group => "root",
		mode => "u=rwx,go=", # dir
	}

	file "/etc/shorewall/shorewall.conf" {
		state => $const.res.file.state.exists,
		content => golang.template(deploy.readfile("/files/shorewall.conf.tmpl")),
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Notify => Svc["shorewall"],
		Depend => File["/etc/shorewall/"],
	}

	svc "shorewall" {
		state => "running",
		startup => "enabled",
	}

	$vardir = local.vardir("shorewall/")

	# Add the default fw zone.
	include zone("fw", struct{
		type => "firewall",
	})

	include params_base # TODO: Do we need the base file present?
}

class firewall:zone_base() {
	file "${vardir}zones.d/" {
		state => $const.res.file.state.exists,
		recurse => true,
		purge => true,
		owner => "root",
		group => "root",
		mode => "u=rwx,go=", # dir
	}

	file "${vardir}zones.header" {
		state => $const.res.file.state.exists,
		content => deploy.readfile("/files/zones"), # static, no template!
		owner => "root",
		group => "root",
		mode => "u=rw,go=",
	}

	file "/etc/shorewall/zones" {
		state => $const.res.file.state.exists,
		fragments => [
			"${vardir}zones.header", # also pull this one file
			"${vardir}zones.d/", # pull from this dir
		],
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Notify => Svc["shorewall"],
		Depend => File["/etc/shorewall/"],
		Depend => File["${vardir}zones.header"],
		Depend => File["${vardir}zones.d/"],
	}
}

# NOTE: the firewall type is added automatically by this module
class firewall:zone($name, $st) {
	# XXX: document why this is named zone_base instead of base:zone_base or
	# change the compiler to use the second version?
	include zone_base

	$type = $st->type || "ipv4"
	#$options = [] # TODO: add option validation?
	$comment = $st->comment || ""

	# TODO: Test type is valid from:
	#$valid_types = [
	#	"bport",
	#	"bport4",
	#	"bport6",
	#	"firewall",
	#	"ip",
	#	"ipsec",
	#	"ipsec4",
	#	"ipsec6",
	#	"ipv4",
	#	"ipv6",
	#	"local"
	#	"loopback"
	#	"vserver",
	#]

	$tmpl = struct{
		name => "${name}",
		type => "${type}",
		comment => "${comment}",
	}
	file "${vardir}zones.d/${name}.zone" {
		state => $const.res.file.state.exists,
		content => golang.template(deploy.readfile("/files/zones.frag.tmpl"), $tmpl),
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Depend => File["${vardir}zones.d/"],
	}
}

class firewall:interface_base() {
	file "${vardir}interfaces.d/" {
		state => $const.res.file.state.exists,
		recurse => true,
		purge => true,
		owner => "root",
		group => "root",
		mode => "u=rwx,go=", # dir
	}

	file "${vardir}interfaces.header" {
		state => $const.res.file.state.exists,
		content => deploy.readfile("/files/interfaces"), # static, no template!
		owner => "root",
		group => "root",
		mode => "u=rw,go=",
	}

	file "/etc/shorewall/interfaces" {
		state => $const.res.file.state.exists,
		fragments => [
			"${vardir}interfaces.header", # also pull this one file
			"${vardir}interfaces.d/", # pull from this dir
		],
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Notify => Svc["shorewall"],
		Depend => File["/etc/shorewall/"],
		Depend => File["${vardir}interfaces.header"],
		Depend => File["${vardir}interfaces.d/"],
	}
}

class firewall:interface($name, $zone, $st) {
	include interface_base

	$interface = $st->interface || (golang_strings.to_upper($zone) + "_IF") # eg: NET_IF
	$physical = $st->physical || $name
	$options []str = $st->options || [] # TODO: add option validation?
	$comment = $st->comment || ""

	$tmpl = struct{
		zone => "${zone}",
		interface => "${interface}",
		physical => "${physical}",
		options => $options,
		comment => "${comment}",
	}
	file "${vardir}interfaces.d/${name}.interface" {
		state => $const.res.file.state.exists,
		content => golang.template(deploy.readfile("/files/interfaces.frag.tmpl"), $tmpl),
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Depend => File["${vardir}interfaces.d/"],
	}
}

class firewall:policy_base() {
	file "${vardir}policy.d/" {
		state => $const.res.file.state.exists,
		recurse => true,
		purge => true,
		owner => "root",
		group => "root",
		mode => "u=rwx,go=", # dir
	}

	file "${vardir}policy.header" {
		state => $const.res.file.state.exists,
		content => deploy.readfile("/files/policy"), # static, no template!
		owner => "root",
		group => "root",
		mode => "u=rw,go=",
	}

	file "/etc/shorewall/policy" {
		state => $const.res.file.state.exists,
		fragments => [
			"${vardir}policy.header", # also pull this one file
			"${vardir}policy.d/", # pull from this dir
		],
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Notify => Svc["shorewall"],
		Depend => File["/etc/shorewall/"],
		Depend => File["${vardir}policy.header"],
		Depend => File["${vardir}policy.d/"],
	}
}

class firewall:policy($name, $st) {
	include policy_base

	$source = $st->source
	$dest = $st->dest
	$policy = $st->policy
	$log = $st->log || false
	$comment = $st->comment || ""

	$tmpl = struct{
		source => "${source}",
		dest => "${dest}",
		policy => "${policy}",
		log_level => $log,
		comment => "${comment}",
	}
	file "${vardir}policy.d/${name}.policy" {
		state => $const.res.file.state.exists,
		content => golang.template(deploy.readfile("/files/policy.frag.tmpl"), $tmpl),
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Depend => File["${vardir}policy.d/"],
	}
}

class firewall:rule_base() {
	file "${vardir}rules.d/" {
		state => $const.res.file.state.exists,
		recurse => true,
		purge => true,
		owner => "root",
		group => "root",
		mode => "u=rwx,go=", # dir
	}

	file "${vardir}rules.header" {
		state => $const.res.file.state.exists,
		content => deploy.readfile("/files/rules"), # static, no template!
		owner => "root",
		group => "root",
		mode => "u=rw,go=",
	}

	file "/etc/shorewall/rules" {
		state => $const.res.file.state.exists,
		fragments => [
			"${vardir}rules.header", # also pull this one file
			"${vardir}rules.d/", # pull from this dir
		],
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Notify => Svc["shorewall"],
		Depend => File["/etc/shorewall/"],
		Depend => File["${vardir}rules.header"],
		Depend => File["${vardir}rules.d/"],
	}
}

class firewall:rule($name, $st) {
	include rule_base

	$rule = $st->rule || "" # entire rule contents OR use the below values

	$action = $st->action || "" # REJECT or SSH(ACCEPT) or Ping(DROP)
	$source = $st->source || "" # source zone
	$source_ips []str = $st->source_ips || []
	$dest = $st->dest || "" # dest zone
	$dest_ips []str = $st->dest_ips || []
	$proto = $st->proto || "" # protocol
	# TODO: port doesn't support ranges atm
	$port = $st->port || 0
	#$sport = $st->sport || 0 # TODO
	#$original = $st->original || [] # TODO
	$comment = $st->comment || ""

	$source_ips_joined = golang_strings.join($source_ips, ",")
	$valid_source = if $source_ips_joined == "" {
		"${source}"
	} else {
		"${source}:${source_ips_joined}"
	}

	$dest_ips_joined = golang_strings.join($dest_ips, ",")
	$valid_dest = if $dest_ips_joined == "" {
		"${dest}"
	} else {
		"${dest}:${dest_ips_joined}"
	}

	$valid_proto = if $proto == "" {
		"-"
	} else {
		"${proto}"
	}

	# TODO: type switch here if we ever support doing that
	$valid_port = if $port == 0 {
		"-"
	} else {
		fmt.printf("%d", $port)
	}

	# TODO: tabs for beautifying, replace with a padding function eventually.
	$full_rule = if $proto == "" and $port == 0 {
		"${action}\t${valid_source}\t\t${valid_dest}"
	} else {
		"${action}\t${valid_source}\t\t${valid_dest}\t\t${valid_proto}\t${valid_port}"
	}

	$valid_rule = if $rule == "" {
		$full_rule
	} else {
		$rule
	}

	$tmpl = struct{
		rule => "${valid_rule}",
		comment => "${comment}",
	}
	file "${vardir}rules.d/${name}.rule" {
		state => $const.res.file.state.exists,
		content => golang.template(deploy.readfile("/files/rules.frag.tmpl"), $tmpl),
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Depend => File["${vardir}rules.d/"],
	}
}

class firewall:bulkrules($name, $st) {
	include rule_base

	$content = $st->content
	# TODO: prepend a comment?

	file "${vardir}rules.d/${name}.rule" {
		state => $const.res.file.state.exists,
		content => $content,
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Depend => File["${vardir}rules.d/"],
	}
}

class firewall:stoppedrule_base() {
	file "${vardir}stoppedrules.d/" {
		state => $const.res.file.state.exists,
		recurse => true,
		purge => true,
		owner => "root",
		group => "root",
		mode => "u=rwx,go=", # dir
	}

	file "${vardir}stoppedrules.header" {
		state => $const.res.file.state.exists,
		content => deploy.readfile("/files/stoppedrules"), # static, no template!
		owner => "root",
		group => "root",
		mode => "u=rw,go=",
	}

	file "/etc/shorewall/stoppedrules" {
		state => $const.res.file.state.exists,
		fragments => [
			"${vardir}stoppedrules.header", # also pull this one file
			"${vardir}stoppedrules.d/", # pull from this dir
		],
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Notify => Svc["shorewall"],
		Depend => File["/etc/shorewall/"],
		Depend => File["${vardir}stoppedrules.header"],
		Depend => File["${vardir}stoppedrules.d/"],
	}
}

class firewall:stoppedrule($name, $st) {
	include stoppedrule_base

	$rule = $st->rule || "" # entire rule contents OR use the below values

	$action = $st->action # REJECT or SSH(ACCEPT) or Ping(DROP)
	$source = $st->source # source zone
	$dest = $st->dest # dest zone

	$comment = $st->comment || ""

	# TODO: tabs for beautifying, replace with a padding function eventually.
	$valid_rule = if $rule == "" {
		"${action}\t${source}\t\t${dest}"
	} else {
		$rule
	}

	$tmpl = struct{
		rule => "${valid_rule}",
		comment => "${comment}",
	}
	file "${vardir}stoppedrules.d/${name}.stoppedrule" {
		state => $const.res.file.state.exists,
		content => golang.template(deploy.readfile("/files/stoppedrules.frag.tmpl"), $tmpl),
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Depend => File["${vardir}stoppedrules.d/"],
	}
}

class firewall:snat_base() {
	file "${vardir}snat.d/" {
		state => $const.res.file.state.exists,
		recurse => true,
		purge => true,
		owner => "root",
		group => "root",
		mode => "u=rwx,go=", # dir
	}

	file "${vardir}snat.header" {
		state => $const.res.file.state.exists,
		content => deploy.readfile("/files/snat"), # static, no template!
		owner => "root",
		group => "root",
		mode => "u=rw,go=",
	}

	file "/etc/shorewall/snat" {
		state => $const.res.file.state.exists,
		fragments => [
			"${vardir}snat.header", # also pull this one file
			"${vardir}snat.d/", # pull from this dir
		],
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Notify => Svc["shorewall"],
		Depend => File["/etc/shorewall/"],
		Depend => File["${vardir}snat.header"],
		Depend => File["${vardir}snat.d/"],
	}
}

class firewall:snat($name, $st) {
	include snat_base

	$rule = $st->rule || "" # entire rule contents OR use the below values

	$action = $st->action # "MASQUERADE" usually
	$source = $st->source # list of ip/cidr
	$dest = $st->dest
	$proto = $st->proto || "" # protocol
	# TODO: port doesn't support ranges atm
	$port = $st->port || 0
	$comment = $st->comment || ""

	$valid_source = golang_strings.join($source, ",")

	$valid_proto = if $proto == "" {
		"-"
	} else {
		"${proto}"
	}

	# TODO: type switch here if we ever support doing that
	$valid_port = if $port == 0 {
		"-"
	} else {
		fmt.printf("%d", $port)
	}

	# TODO: tabs for beautifying, replace with a padding function eventually.
	$full_rule = if $proto == "" and $port == 0 {
		"${action}\t${valid_source}\t\t${dest}"
	} else {
		"${action}\t${valid_source}\t\t${dest}\t\t${valid_proto}\t${valid_port}"
	}

	$valid_rule = if $rule == "" {
		$full_rule
	} else {
		$rule
	}

	$tmpl = struct{
		rule => "${valid_rule}",
		#action => "${action}",
		#source => "${valid_source}",
		#dest => "${dest}",
		comment => "${comment}",
	}
	file "${vardir}snat.d/${name}.snat" {
		state => $const.res.file.state.exists,
		content => golang.template(deploy.readfile("/files/snat.frag.tmpl"), $tmpl),
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Depend => File["${vardir}snat.d/"],
	}
}

class firewall:params_base() {
	file "${vardir}params.d/" {
		state => $const.res.file.state.exists,
		recurse => true,
		purge => true,
		owner => "root",
		group => "root",
		mode => "u=rwx,go=", # dir
	}

	file "${vardir}params.header" {
		state => $const.res.file.state.exists,
		content => deploy.readfile("/files/params"), # static, no template!
		owner => "root",
		group => "root",
		mode => "u=rw,go=",
	}

	file "/etc/shorewall/params" {
		state => $const.res.file.state.exists,
		fragments => [
			"${vardir}params.header", # also pull this one file
			"${vardir}params.d/", # pull from this dir
		],
		owner => "root",
		group => "root",
		mode => "u=rw,go=",

		Notify => Svc["shorewall"],
		Depend => File["/etc/shorewall/"],
		Depend => File["${vardir}params.header"],
		Depend => File["${vardir}params.d/"],
	}
}

class firewall:params($name, $st) {
	include params_base

	# TODO: add params
}
