package main

import (
	"errors"
	"fmt"
	"io"
	"net"
	"strings"
	"time"

	iradix "github.com/hashicorp/go-immutable-radix"
	"github.com/jedisct1/dlog"
	"github.com/miekg/dns"
)

type PluginBlockIP struct {
	blockedPrefixes *iradix.Tree
	blockedIPs      map[string]interface{}
	logger          io.Writer
	format          string
}

func (plugin *PluginBlockIP) Name() string {
	return "block_ip"
}

func (plugin *PluginBlockIP) Description() string {
	return "Block responses containing specific IP addresses"
}

func (plugin *PluginBlockIP) Init(proxy *Proxy) error {
	dlog.Noticef("Loading the set of IP blocking rules from [%s]", proxy.blockIPFile)
	lines, err := ReadTextFile(proxy.blockIPFile)
	if err != nil {
		return err
	}
	plugin.blockedPrefixes = iradix.New()
	plugin.blockedIPs = make(map[string]interface{})
	for lineNo, line := range strings.Split(lines, "\n") {
		line = TrimAndStripInlineComments(line)
		if len(line) == 0 {
			continue
		}
		ip := net.ParseIP(line)
		trailingStar := strings.HasSuffix(line, "*")
		if len(line) < 2 || (ip != nil && trailingStar) {
			dlog.Errorf("Suspicious IP blocking rule [%s] at line %d", line, lineNo)
			continue
		}
		if trailingStar {
			line = line[:len(line)-1]
		}
		if strings.HasSuffix(line, ":") || strings.HasSuffix(line, ".") {
			line = line[:len(line)-1]
		}
		if len(line) == 0 {
			dlog.Errorf("Empty IP blocking rule at line %d", lineNo)
			continue
		}
		if strings.Contains(line, "*") {
			dlog.Errorf("Invalid rule: [%s] - wildcards can only be used as a suffix at line %d", line, lineNo)
			continue
		}
		line = strings.ToLower(line)
		if trailingStar {
			plugin.blockedPrefixes, _, _ = plugin.blockedPrefixes.Insert([]byte(line), 0)
		} else {
			plugin.blockedIPs[line] = true
		}
	}
	if len(proxy.blockIPLogFile) == 0 {
		return nil
	}
	plugin.logger = Logger(proxy.logMaxSize, proxy.logMaxAge, proxy.logMaxBackups, proxy.blockIPLogFile)
	plugin.format = proxy.blockIPFormat

	return nil
}

func (plugin *PluginBlockIP) Drop() error {
	return nil
}

func (plugin *PluginBlockIP) Reload() error {
	return nil
}

func (plugin *PluginBlockIP) Eval(pluginsState *PluginsState, msg *dns.Msg) error {
	if pluginsState.sessionData["whitelisted"] != nil {
		return nil
	}
	answers := msg.Answer
	if len(answers) == 0 {
		return nil
	}
	reject, reason, ipStr := false, "", ""
	for _, answer := range answers {
		header := answer.Header()
		Rrtype := header.Rrtype
		if header.Class != dns.ClassINET || (Rrtype != dns.TypeA && Rrtype != dns.TypeAAAA) {
			continue
		}
		if Rrtype == dns.TypeA {
			ipStr = answer.(*dns.A).A.String()
		} else if Rrtype == dns.TypeAAAA {
			ipStr = answer.(*dns.AAAA).AAAA.String() // IPv4-mapped IPv6 addresses are converted to IPv4
		}
		if _, found := plugin.blockedIPs[ipStr]; found {
			reject, reason = true, ipStr
			break
		}
		match, _, found := plugin.blockedPrefixes.Root().LongestPrefix([]byte(ipStr))
		if found {
			if len(match) == len(ipStr) || (ipStr[len(match)] == '.' || ipStr[len(match)] == ':') {
				reject, reason = true, string(match)+"*"
				break
			}
		}
	}
	if reject {
		pluginsState.action = PluginsActionReject
		pluginsState.returnCode = PluginsReturnCodeReject
		if plugin.logger != nil {
			qName := pluginsState.qName
			var clientIPStr string
			switch pluginsState.clientProto {
			case "udp":
				clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
			case "tcp", "local_doh":
				clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
			default:
				// Ignore internal flow.
				return nil
			}
			var line string
			if plugin.format == "tsv" {
				now := time.Now()
				year, month, day := now.Date()
				hour, minute, second := now.Clock()
				tsStr := fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d]", year, int(month), day, hour, minute, second)
				line = fmt.Sprintf(
					"%s\t%s\t%s\t%s\t%s\n",
					tsStr,
					clientIPStr,
					StringQuote(qName),
					StringQuote(ipStr),
					StringQuote(reason),
				)
			} else if plugin.format == "ltsv" {
				line = fmt.Sprintf("time:%d\thost:%s\tqname:%s\tip:%s\tmessage:%s\n", time.Now().Unix(), clientIPStr, StringQuote(qName), StringQuote(ipStr), StringQuote(reason))
			} else {
				dlog.Fatalf("Unexpected log format: [%s]", plugin.format)
			}
			if plugin.logger == nil {
				return errors.New("Log file not initialized")
			}
			_, _ = plugin.logger.Write([]byte(line))
		}
	}
	return nil
}
