#!/bin/sh /etc/rc.common
#
# Copyright (C) 2019 p.t. <peter-tank@github.com>
#
# This is free software, licensed under the GNU General Public License v3.
# See /LICENSE for more information.
#

START=30

USE_PROCD=1
INIT_TRACE=
EXTRA_COMMANDS=

PROG=/usr/sbin/dnscrypt-proxy
CONFIG_DIR=/var/etc
USER=nobody
NAME=dnscrypt-proxy

SERVICE_DAEMONIZE=
SERVICE_USE_PID=1
SERVICE_MATCH_EXEC=1
SERVICE_STOP_TIME=15
SERVICE_PID_FILE=/var/run/$NAME.pid

LUCI_STATUS=/var/run/luci-reload-status
LOG_FILE="/var/etc/dnscrypt"
CACHE_DIR="/usr/share/dnscrypt-proxy"
DIRECT_IPSETS="vpsiplist localnetwork ss_spec_wan_ac"

# has side effects when there is a option names the same 'addrs' in shadowsocksr config file
# keep this uci function excuted at the bottom of scripts
uci_filter_resolver_addrs() {
	local __PKGNAME="${NAME}"
	local __SECTIONTYPE="server_addr"
	local ret

	filter_resolver_addrs()
	{
		local __TYPE="$1"
		local __OPTION="$2"
		set | sed -ne "s/^CONFIG_.*_addrs='\([^']*\)'$/\1/p" | sed -e "s/[, ]/\n/g" | sort | uniq
	}

	# config_load "${__PKGNAME}"
	# tricky way first for all server_addr addrs in DNSCrypt ACL
	ret=$(filter_resolver_addrs "${__SECTIONTYPE}" "addrs")
	# or get all addrs from all resolvers configured.
	[ -z "${ret}" ] && {
		CONFIG_APPEND=1
		config_list_foreach "ns1" "resolvers" config_load
		ret=$(filter_resolver_addrs ".*" "addrs")
	}
	echo "${ret}"
}

uci_get_by_name() {
	local ret=$(uci get dnscrypt-proxy.$1.$2 2>/dev/null)
	echo "${ret:=$3}"
}

boot() {
    dnscrypt_boot=1
    rc_procd start_service
}

dnscrypt_instance() {
    local config_path="$CONFIG_DIR/dnscrypt-proxy-$1.conf"

    [ $(uci_get_by_name "$1" enable 0) -eq 1 ] || return 0
    create_config_file $1 "$config_path"

    procd_open_instance
    procd_set_param command $PROG -config "$config_path" -pidfile "$SERVICE_PID_FILE"
    procd_set_param file "$config_path"
    procd_close_instance
}

include_for_firewall_reload() {
	local FWI=$(uci get firewall.dnscrypt_proxy.path 2>/dev/null)

	[ -n "$FWI" ] || return 0
	echo '#!/bin/sh' >$FWI
	cat <<-EOF >>"$FWI"
    addr_filter=\$(uci get ${NAME}.ns1.addr_filter 2>/dev/null)
    if [ "\${addr_filter:-auto}" == "auto" ]; then
        for list in ${DIRECT_IPSETS}; do
            ipset list -n "\${list}" >/dev/null 2>&1
            [ \$? -eq 0 ] && addr_filter="\${list}" && break
        done
    fi
    ipset list -n "\${addr_filter}" >/dev/null 2>&1 && {
        ipset_shell=\$(find ${CONFIG_DIR} -name "dnscrypt_filter2_*.sh" | head -n1)
        [ -z "\${ipset_shell}" ] || {
        sed -i 's/^ipset add [^ ]* \(.*\)$/ipset add '"\${addr_filter}"' \1/g' "\${ipset_shell}"
        echo "### \$(date) on firewall reload" >> "${LOG_FILE}.log"
        echo "### appending resolver addresses to ipset list \${addr_filter}..." >> "${LOG_FILE}.log"
        wc -l "\${ipset_shell}" >> "${LOG_FILE}.log" 2>&1
        date >"\${ipset_shell}.log"
	\${ipset_shell} >>"\${ipset_shell}.log" 2>&1 &
        }
    }
	EOF
}

create_config_file() {
    local server_names listen_address query_meta max_clients bootstrap_resolvers netprobe_address
    local disabled_server_names log_file log_level timeout keepalive cert_refresh_delay netprobe_timeout
    local log_files_max_size log_files_max_age log_files_max_backups reject_ttl
    local reject_ttl cache_size cache_min_ttl cache_max_ttl cache_neg_min_ttl cache_neg_max_ttl
    local lb_strategy blocked_query_response force_defaults force_gets
    local proxy http_proxy cloaking_rules cloak_ttl
    local config_path="$2"
    local dstf two_line_header forward_resolvers

    [ ! -d "$CONFIG_DIR" ] && mkdir -p "$CONFIG_DIR"
    [ -d "${CACHE_DIR}" ] || mkdir -p "${CACHE_DIR}"
    [ -f "$config_path" ] && rm "$config_path"

    config_get      server_names            $1 'server_names'        ''
    config_get      listen_addresses        $1 'listen_addresses'    '127.0.0.1:5335'
    config_get      query_meta              $1 'query_meta'          ''
    config_get      max_clients             $1 'max_clients'         '250'
    config_get      bootstrap_resolvers     $1 'bootstrap_resolvers' '114.114.114.114:53'
    config_get      netprobe_address        $1 'netprobe_address'    '114.114.114.114:53'
    config_get      disabled_server_names   $1 'disabled_server_names' ''

    config_get      log_file                $1 'log_file'            "${LOG_FILE}.log"
    config_get      log_level               $1 'log_level'           '2'

    config_get      timeout                 $1 'timeout'             '5000'
    config_get      keepalive               $1 'keepalive'           '30'
    config_get      cert_refresh_delay      $1 'cert_refresh_delay'  '240'
    config_get      netprobe_timeout        $1 'netprobe_timeout'    '60'
    config_get      log_files_max_size      $1 'log_files_max_size'  '1'
    config_get      log_files_max_age       $1 'log_files_max_age'   '7'
    config_get      log_files_max_backups   $1 'log_files_max_backups' '1'
    config_get      reject_ttl              $1 'reject_ttl'          '600'
    config_get      cache_size              $1 'cache_size'          '512'
    config_get      cache_min_ttl           $1 'cache_min_ttl'       '600'
    config_get      cache_max_ttl           $1 'cache_max_ttl'       '86400'
    config_get      cache_neg_min_ttl       $1 'cache_neg_min_ttl'   '60'
    config_get      cache_neg_max_ttl       $1 'cache_neg_max_ttl'   '600'

    config_get      lb_strategy             $1 'lb_strategy'         'p2'
    config_get      blocked_query_response  $1 'blocked_query_response' 'hinfo'

    config_get      proxy                   $1 'proxy'               ''
    config_get      http_proxy              $1 'http_proxy'          ''

    config_get      cloak_ttl               $1 'cloak_ttl'           '600'

    append_str_param        "user_name"           "$USER"                 $config_path
    append_multivalue_param "server_names"        "$server_names"         $config_path
    append_multivalue_param "listen_addresses"    "$listen_addresses"     $config_path
    append_multivalue_param "query_meta"          "$query_meta"           $config_path
    append_param            "max_clients"         "$max_clients"          $config_path
    append_multivalue_param "bootstrap_resolvers" "$bootstrap_resolvers"    $config_path
    append_str_param        "netprobe_address"    "$netprobe_address"     $config_path
    append_multivalue_param "disabled_server_names" "$disabled_server_names" $config_path

    append_param            "log_level"           "$log_level"            $config_path
    append_str_param        "log_file"            "$log_file"             $config_path

    append_param            "timeout"             "$timeout"              $config_path
    append_param            "cert_refresh_delay"  "$cert_refresh_delay"   $config_path
    append_param            "netprobe_timeout"    "$netprobe_timeout"     $config_path
    append_param            "log_files_max_size"  "$log_files_max_size"   $config_path
    append_param            "log_files_max_age"   "$log_files_max_age"    $config_path
    append_param            "log_files_max_backups" "$log_files_max_backups" $config_path
    append_param            "reject_ttl"          "$reject_ttl"           $config_path
    append_param            "cache_size"          "$cache_size"           $config_path
    append_param            "cache_min_ttl"       "$cache_min_ttl"        $config_path
    append_param            "cache_max_ttl"       "$cache_max_ttl"        $config_path
    append_param            "cache_neg_min_ttl"   "$cache_neg_min_ttl"    $config_path
    append_param            "cache_neg_max_ttl"   "$cache_neg_max_ttl"    $config_path

    append_str_param        "lb_strategy"         "$lb_strategy"          $config_path
    append_str_param        "blocked_query_response" "$blocked_query_response" $config_path

    append_str_param        "proxy"               "$proxy"                $config_path
    append_str_param        "http_proxy"          "$http_proxy"           $config_path

    append_param            "cloak_ttl"           "$cloak_ttl"            $config_path

    force_defaults='
lb_estimator,true
ignore_system_dns,true
block_unqualified,true
block_undelegated,true
ipv4_servers,true
ipv6_servers,false
block_ipv6,true
dnscrypt_servers,true
doh_servers,true
require_dnssec,false
force_tcp,false
require_nolog,true
require_nofilter,true
cache,true
offline_mode,false
dnscrypt_ephemeral_keys,false
tls_disable_session_tickets,false
cert_ignore_timestamp,true
'
    append_yes_no() {
        local sets=$1
        local defs=$2
        local config_path=$3
        local line param_name param_value val

        for line in $defs; do
            param_name="${line%%,*}"; param_value="${line##*,}";
            for val in $sets; do
                [[ x$val == x$param_name ]] && param_value=true
            done
            echo "$param_name = $param_value" >> $config_path
        done
    }
    config_get force_gets $1 'force' ''
    append_yes_no "$force_gets" "$force_defaults" "$config_path"

    handle_list_file() {
        local file
        local fwd="$2"
        if [ x"${1:0:4}" == x"http" ]; then file=$(cache_file "${1}" 0);
        elif [ x"${1:0:1}" == x"/" ]; then file="${1}";
        else file="$CACHE_DIR/${1}";
        fi
        [ x"$file" == x -o ! -f "${file}" ] && return
        local tmpf="/tmp/dnscrypt_${file##*/}"
        if [ x"${file##*.}" == xconf -o x"${file##*.}" == xadblock ]; then
            sed -ne "s/^\(address\|server\)=\/\([^/]*\)\/.*$/\2 $fwd/p" -e "s/ *$//g" "$file" > "$tmpf"
        else
            [ -z "$fwd" ] || sed -i -e "s/$/ $fwd/g" "$tmpf"
            cat "$file" > "$tmpf"
        fi
        echo "$tmpf"
    }

    append_list_file() {
        local sec=$1
        local list=$2
        local param_name=$3
        local txtf=$4
        local log=$5
        local config_path=$6
        local line2=$7
        local fwd=$8

        echo -e $line2  > "$txtf"
        [ -z "$log" ] || {
            append_str_param "log_file"   "${LOG_FILE}_$log" $config_path
            append_str_param "log_format" "tsv"              $config_path
        }
        merg_list_file() {
            local src=${1%% *}
            local list_file

            list_file=$(handle_list_file $src "$fwd")
            [[ x"$list_file" == x ]] && return 0
            cat "$list_file" >> "$txtf"
        }
        config_list_foreach "$sec" "$list" merg_list_file $config_path
        line=$(sort "$txtf" | uniq); echo "$line" > "$txtf"
        line=$(cat "$txtf" | wc -l); [ $line -le 2 ] && rm -f "$txtf" && return
        echo "#total $((line-1)) uniq items." >> "$txtf"
        append_str_param "$param_name" "$txtf" $config_path
    }

    dstf="$CONFIG_DIR/dnscrypt_forwarding_rules.txt"
    two_line_header='#dnscrypt forwarding rule file.\n'
    forward_resolvers="$bootstrap_resolvers"
    append_list_file $1 "forwarding_rules" "forwarding_rules" "$dstf" "" $config_path "$two_line_header" $forward_resolvers

    dstf="$CONFIG_DIR/dnscrypt_cloaking_rules.txt"
    two_line_header='#dnscrypt cloaking rules file.\n'
    append_list_file $1 "cloaking_rules" "cloaking_rules" "$dstf" "" $config_path "$two_line_header"

    echo "[query_log]" >> $config_path
    append_str_param        "file"    "${LOG_FILE}_query.log"   $config_path
    append_str_param        "format"  "tsv"                     $config_path

    echo "[nx_log]" >> $config_path
    append_str_param        "file"    "${LOG_FILE}_nx.log"      $config_path
    append_str_param        "format"  "tsv"                     $config_path

    echo "[blocked_names]" >> $config_path
    dstf="$CONFIG_DIR/dnscrypt_blocked_names.txt"
    two_line_header='#dnscrypt domain black list file.\n*.test\n*.onion\n*.localhost\n*.local\n*.invalid\n*.bind\n*.lan\n*.internal\n*.intranet\n*.private\n*.workgroup\n*.10.in-addr.arpa\n*.16.172.in-addr.arpa\n*.168.192.in-addr.arpa\n*.254.169.in-addr.arpa\n*.d.f.ip6.arpa'
    append_list_file $1 "blocked_names" "blocked_names_file" "$dstf" "bd.log" $config_path "$two_line_header"

    echo "[blocked_ips]" >> $config_path
    dstf="$CONFIG_DIR/dnscrypt_blocked_ips.txt"
    two_line_header='#dnscrypt ip black list file.\n127.*\n192.168.*'
    append_list_file $1 "blocked_ips" "blocked_ips_file" "$dstf" "bi.log" $config_path "$two_line_header"

    echo "[allowed_names]" >> $config_path
    dstf="$CONFIG_DIR/dnscrypt_allowed_names.txt"
    two_line_header='#dnscrypt white list file.\n'
    append_list_file $1 "allowed_names" "allowed_names_file" "$dstf" "wd.log" $config_path "$two_line_header"

    echo "[static]" >> $config_path
    append_static_resolver() {
        local sdns=$1
        local config_path=$2
        local sdns

        [[ "x$sdns" == x ]] && return 0
        echo "[static.'$sdns']"        >> $config_path
        append_str_param        "sdns"         "$sdns"          $config_path
    }
    config_list_foreach "$1" "sdns" append_static_resolver $config_path

    echo "[sources]" >> $config_path
    append_resolvers_source() {
        local resolver=$1
        local config_path=$2
        local section="global"
        local urls cache_file minisign_key prefix selfsign details_json

        config_unset $section "urls"
        [ -f "/etc/config/${resolver}" ] || return 0
        config_load ${resolver}
        [ $? -eq 0 ] || return 0
        config_get      urls                    $section 'urls'          ''
        [[ "x$urls" == x ]] && return 0
        echo "[sources.'${resolver}']" >> $config_path

        config_get      urls                    $section 'urls'          ''
        config_get      format                  $section 'format'        'v2'
        config_get      cache_file              $section 'cache_file'    "${urls##*/}"
        config_get      minisign_key            $section 'minisign_key'  'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
        config_get      prefix                  $section 'prefix'        ''

        config_get      details_json            $section 'details_json'  "${cache_file%%.md}.json"
        config_get_bool cache_mode              $section 'cache_mode'    '1'
        config_get_bool selfsign                $section 'selfsign'      '0'

        if [ $cache_mode -eq 1 ] ; then
            local redirect_https site_addr schema port
            redirect_https="$(uci_get uhttpd     main       redirect_https     '0')"
            site_addr="$(uci_get      uhttpd     main       listen_https        '[::]:80')"
            schema=http
            port=${site_addr##*:}
            [ x"${port}" != "x80" -a $redirect_https -eq 1 ] && opkg list-installed | grep libustream-openssl && schema=https
            [ x"${port}" == "x80" -o x"${port}" == "x443" ] && port=
            urls="${schema}://localhost${port:+:${port}}/${urls##*/}"
        fi

        append_multivalue_param "urls"          "$urls"                  $config_path
        append_str_param        "format"        "$format"                $config_path
        append_str_param        "cache_file"    "$cache_file"            $config_path
        append_str_param        "minisign_key"  "$minisign_key"          $config_path
        append_str_param        "prefix"        "$prefix"                $config_path

        config_get_bool selfsign                $section 'selfsign'      '0'
        config_get      details_json            $section 'details_json'  ''
    }
    config_list_foreach "$1" "resolvers" append_resolvers_source $config_path

}

multivalue_parse() {
    local param_values=$(echo $1 | sed -e 's/[, ]/\n/g' -e 's/\n\n/\n/g')

    [[ x == "x${param_values}" ]] && echo -n "[]" && return 1
    local p
    p=$(for p in ${param_values}; do echo -n "'${p}'"; done)
    echo -n "["
    echo -n $p | sed -e "s/''/', '/g"
    echo -n "]"
}

log_ignored_param() {
    local param_name=$1
    logger -t dnscrypt-proxy -p user.warn "dnscrypt-proxy plugins support not present, ignoring '$param_name' parameter."
}

append_multivalue_param() {
    local param_name=$1
    local param_value=$2
    local config_path=$3
	
    echo "$param_name = $(multivalue_parse $param_value)" >> $config_path
}

append_str_param() {
    local param_name=$1
    local param_value=$2
    local config_path=$3
	
    echo "$param_name = '$param_value'" >> $config_path
}

append_param() {
    local param_name=$1
    local param_value=$2
    local config_path=$3
	
    echo "$param_name = $param_value" >> $config_path
}

start_service() {
    config_load dnscrypt-proxy

    local addr_filter list
    config_get      addr_filter               ns1 'addr_filter'           'auto'
    if [ "${addr_filter}" == "auto" ]; then
        for list in ${DIRECT_IPSETS}; do
            ipset list -n "${list}" >/dev/null 2>&1
            [ $? -eq 0 ] && addr_filter="${list}" && break
        done
    fi
    ipset list -n "${addr_filter}" >/dev/null 2>&1 && {
        local ipset_shell="${CONFIG_DIR}/dnscrypt_filter2_${addr_filter}.sh"
        uci_filter_resolver_addrs > "${ipset_shell}"
        sed -i 's/^\(.*\)$/ipset add '"${addr_filter}"' "\1" nomatch/g' "${ipset_shell}"
        chmod +x "${ipset_shell}"
        echo "### appending resolver addresses to ipset list ${addr_filter}..." >> "${LOG_FILE}.log"
        wc -l "${ipset_shell}" >> "${LOG_FILE}.log" 2>&1
        date >"${ipset_shell}.log"
	${ipset_shell} >>"${ipset_shell}.log" 2>&1 &
    }
    include_for_firewall_reload

    config_foreach dnscrypt_instance dnscrypt-proxy
}

service_triggers() {
    procd_add_reload_trigger 'dnscrypt-proxy'
}

reload_service() {
    stop
    start
}

cache_file() {
    local tdl=$1
    local key=$2
    local root=$3
    local mode=${4:-0}
    [ -n "$root" ] && tdl=$tdl$'\n'$tdl.minisig
    for cdl in $tdl; do
    echo "INFO: details[$cdl]" > $LUCI_STATUS && sleep 3
    local md5=$(echo $cdl | md5sum | cut -d' ' -f1)
    local fn="$CACHE_DIR/${md5}_${cdl##*/}"
    local tmpf="/tmp/dnscrypt_${cdl##*/}.dl"
    local line
    [ ! -f "$fn" ] && {
        wget -q --no-check-certificate -t 3 -T 30 -O "$tmpf" "$cdl"
        [ $? -eq 0 ] || return
        line=$(cat "$tmpf" | wc -l)
        [ $line -gt 1 ] || return
        cat /dev/null > "$fn"
        [ -z "$key" -o $mode -eq 11 ] && echo "##source[#$line]: $cdl" >> "$fn"
        cat "$tmpf" >> "$fn"
    }
    [ $mode -ge 10 ] && cat "$fn" > $root/${cdl##*/}
    done
    echo "${fn%%.minisig}"
}

