(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)

open Dns

module N = Domain_name.Set

let src = Logs.Src.create "dns_resolver_cache" ~doc:"DNS resolver cache"
module Log = (val Logs.src_log src : Logs.LOG)

let _pp_err ppf = function
  | `Cache_miss -> Fmt.string ppf "cache miss"
  | `Cache_drop -> Fmt.string ppf "cache drop"

let pp_question ppf (name, typ) =
  Fmt.pf ppf "%a (%a)" Domain_name.pp name Packet.Question.pp_qtype typ

let is_signed = function
  | Dns_cache.AuthoritativeAnswer signed
  | AuthoritativeAuthority signed -> signed
  | _ -> None

let find_nsec t ts typ name =
  let rec up name =
    match snd (Dns_cache.get t ts name Nsec) with
    | Ok (`Entry (ttl, nsec), rank) ->
      if Bit_map.mem (Rr_map.to_int typ) nsec.Nsec.types then
        Some (name, (ttl, nsec), rank)
      else
        None
    | _ ->
      if Domain_name.count_labels name >= 1 then
        up (Domain_name.drop_label_exn name)
      else
        None
  in
  up name

let nsec_no t ts typ name =
  match find_nsec t ts typ name with
  | Some _ -> true
  | None -> false

let find_nsec3 t ts typ name =
  let rec up name =
    match snd (Dns_cache.get_nsec3 t ts name) with
    | Ok nsec3 ->
      let (_, _, Nsec3.{ iterations ; salt ; _ }, _) = List.hd nsec3 in
      let soa_name = Domain_name.drop_label_exn name in
      let hashed_name = Dnssec.nsec3_hashed_name salt iterations ~soa_name name in
      List.find_opt (fun (name, _, nsec3, _) ->
          let name = Domain_name.drop_label_exn ~rev:true name in
          let hashed_next_owner =
            Domain_name.prepend_label_exn soa_name
              (Base32.encode nsec3.Nsec3.next_owner_hashed)
          in
          (* TODO non-wc-expanded nsec3 only?? *)
          (Domain_name.compare name hashed_name < 0 &&
           Domain_name.compare hashed_name hashed_next_owner < 0) ||
          (* TODO wc nsec3 as well? *)
          (Domain_name.compare name hashed_name = 0 &&
           not (Bit_map.mem (Rr_map.to_int typ) nsec3.types))
        )
          nsec3
    | Error _ ->
      if Domain_name.count_labels name > 1 then
        up (Domain_name.drop_label_exn name)
      else
        None
  in
  up name

let nsec3_covering t ts typ name =
  match find_nsec3 t ts typ name with
  | None -> false
  | Some _ -> true

let upwards_ds_nonexisting t ts name =
  let rec go name =
    if nsec_no t ts Ds name || nsec3_covering t ts Ds name then
      true
    else
      match Domain_name.drop_label name with
      | Error _ -> false
      | Ok name -> go name
  in
  go name

let find_nearest_ns ip_proto dnssec ts t name =
  let find_ns name = match snd (Dns_cache.get t ts name Ns) with
    | Ok (`Entry (_, names), r) -> Domain_name.Host_set.elements names, is_signed r
    | _ -> [], None
  and find_dnskey name = match snd (Dns_cache.get t ts name Dnskey) with
    | Ok _ -> true
    | _ -> false
  and dnskey_nonexisting name = match snd (Dns_cache.get t ts name Dnskey) with
    | Ok _ -> false
    | Error _ ->
      (* no need to check for Ds nonexistance upwards, since we're only called
         if we have a Ds *)
      nsec_no t ts Dnskey name || nsec3_covering t ts Dnskey name
  and need_to_query_for_ds name = match snd (Dns_cache.get t ts name Ds) with
    | Ok _ -> false
    | Error _ -> not (upwards_ds_nonexisting t ts name)
  and have_ds name =
    match snd (Dns_cache.get t ts name Ds) with
    | Ok (`Entry _, _) -> true
    | _ -> false
  and find_address name =
    let ip4s =
      Result.fold
        ~ok:(function
            | `Entry (_, ips), _ ->
              List.map (fun ip -> Ipaddr.V4 ip) (Ipaddr.V4.Set.elements ips)
            | _ -> [])
        ~error:(fun _ -> [])
        (snd (Dns_cache.get t ts name A))
    and ip6s =
      Result.fold
        ~ok:(function
            | `Entry (_, ips), _ ->
              List.map (fun ip -> Ipaddr.V6 ip) (Ipaddr.V6.Set.elements ips)
            | _ -> [])
        ~error:(fun _ -> [])
        (snd (Dns_cache.get t ts name Aaaa))
    in
    match ip_proto with
    | `Both -> ip4s @ ip6s
    | `Ipv4_only -> ip4s
    | `Ipv6_only -> ip6s
  in
  let have_ips_or_dnskey name ips =
    if dnssec && not (find_dnskey name) && have_ds name then
      if dnskey_nonexisting name then (
        (* this is tricky, and likely bad - we have a DS but no DNSKEY *)
        Log.warn (fun m -> m "DS present for %a, but nonexisting DNSKEY (NSEC/NSEC3)"
                     Domain_name.pp name);
        `HaveIPs (name, ips))
      else
        (* if dnssec is enabled, and have a DS record, and we don't have a dnskey,
           request it -- avoiding loops by only asking for dnskey if there's DS *)
        `NeedDnskey (name, ips)
    else
      `HaveIPs (name, ips)
  in
  let rec go nam =
    (* Log.info (fun m -> m "go %a" Domain_name.pp nam); *)
    let ns, signed_ns = find_ns nam in
    match ns with
    | [] ->
      (* Log.warn (fun m -> m "go no NS for %a" Domain_name.pp nam); *)
      if Domain_name.(equal root nam) then
        [ have_ips_or_dnskey nam (Dns_resolver_root.ips ip_proto) ]
      else
        go (Domain_name.drop_label_exn nam)
    | _ when dnssec && need_to_query_for_ds nam ->
      (* dnssec enabled, and no DS (and no nonexistance proof for DS) ->
         query for DS (which is always provided by the domain above:
         "." has it for ".coop" / ".com" for "example/com"
         -> this also avoids loops, if we get a negative reply for DS, we move
            on (and run into the case below)
      *)
      (* Log.info (fun m -> m "need to query for DS %a" Domain_name.pp nam); *)
      List.map (function
       | `HaveIPs (_name, ips) -> `NeedDs (nam, ips)
       | `NeedDnskey _ | `NeedAddress _ | `NeedDs _
       | `NeedSignedNs _ as r -> r)
        (if Domain_name.(equal root nam) then
           [ have_ips_or_dnskey name (Dns_resolver_root.ips ip_proto) ]
         else
           go (Domain_name.drop_label_exn nam))
    | name_servers ->
      List.fold_left (fun acc ns ->
          let host = Domain_name.raw ns in
          match find_address host with
          | [] ->
            (* Log.info (fun m -> m "go no address for NS %a (for %a)"
                     Domain_name.pp host
                     Domain_name.pp nam); *)
            if Domain_name.is_subdomain ~subdomain:ns ~domain:nam then
              (* we actually need glue *)
              if Domain_name.(equal root nam) then
                have_ips_or_dnskey nam (Dns_resolver_root.ips ip_proto) :: acc
              else
                (go (Domain_name.drop_label_exn nam)) @ acc
            else
              `NeedAddress (nam, host) :: acc
          | ips ->
            (* Log.info (fun m -> m "go address for NS %a (for %a): %a (dnssec %B signed_ns %B have_ds %B find_dnskey %B)"
                     Domain_name.pp host
                     Domain_name.pp nam
                     Ipaddr.pp ip
                     dnssec (Option.is_some signed_ns) (have_ds nam)
                     (find_dnskey nam)); *)
            if dnssec && Option.is_none signed_ns && have_ds nam then
              if find_dnskey nam then
                `NeedSignedNs (nam, ips) :: acc
              else if dnskey_nonexisting nam then (
                (* Log.warn (fun m -> m "DS present for %a, but NSEC/NSEC3 for DNSKEY"
                         Domain_name.pp nam); *)
                have_ips_or_dnskey nam ips :: acc)
              else
                `NeedDnskey (nam, ips) :: acc
            else
              have_ips_or_dnskey nam ips :: acc)
        [] name_servers
  in
  go name

let resolve t ~qname_minimisation ~dnssec ip_proto ts name typ =
  (* the standard recursive algorithm *)
  let addresses = match ip_proto with
    | `Both -> [`K (Rr_map.K A); `K (Rr_map.K Aaaa)]
    | `Ipv4_only -> [`K (Rr_map.K A)]
    | `Ipv6_only -> [`K (Rr_map.K Aaaa)]
  in
  (* with DNSSec:
     - input is qname and qtyp
     - (a) we have (validated) NS record (+DNSKEY) for zone -> move along
     - (b) we miss a NS entry -> drop label and find one
     ---> we also want to collect DS and DNSKEY entries (or non-existence of DS)
     ---> we get DS by dnssec ok in EDNS
     ---> we may have unsigned NS (+ glue), and need to ask the NS for NS (+dnssec)
     ---> we may have unsigned glue, and need to go down for signed A/AAAA
  *)
  let rec go t visited types zone name =
    Log.debug (fun m -> m "go %a (zone %a)" Domain_name.pp name Domain_name.pp zone) ;
    let t =
      if N.mem zone visited then
        (* we need to break the cycle if there's one domain pointing to NS in
           another domain, and this other domain NS pointing to one domain. *)
        (* if we lack glue here, we should query .. for NS again with the hope
           to get some glue *)
        Dns_cache.remove t zone
      else
        t
    in
    List.concat_map (function
        | `NeedAddress (zone, ns) -> go t (N.add zone visited) addresses zone ns
        | `NeedDnskey (zone, ips) -> [ zone, zone, [`K (Rr_map.K Dnskey)], ips, t ]
        | `NeedDs (zone, ips) -> [ zone, zone, [`K (Rr_map.K Ds)], ips, t ]
        | `HaveIPs (zone, ips) ->
          (* qname minimisation: if we can, query minimal qname (and NS)
             this is possible as long as we haven't received a negative reply on
             the NS query -- that's why we have another Dns_cache.get NS below *)
          let name, types =
            if qname_minimisation then
              let n = Domain_name.count_labels name
              and z = Domain_name.count_labels zone
              in
              let n' =
                if succ z < n then
                  Domain_name.drop_label_exn ~amount:(n - succ z) name
                else
                  name
              in
              let name' =
                match snd (Dns_cache.get t ts n' Ns) with
                | Ok (`Entry _, _) -> n'
                | _ -> name
              in
              name', if Domain_name.equal name' name then types else [ `K (Rr_map.K Ns) ]
            else
              name, types
          in
          [ zone, name, types, ips, t ]
        | `NeedSignedNs (domain, ips) -> [ domain, domain, [ `K (Rr_map.K Ns) ], ips, t ])
      (find_nearest_ns ip_proto dnssec ts t (Domain_name.raw name))
  in
  go t N.empty [typ] Domain_name.root name

let to_map (name, soa) = Name_rr_map.singleton name Soa soa

let follow_cname t ts typ ~name ttl ~alias =
  let rec follow t acc name =
    let t, r = Dns_cache.get_or_cname t ts name typ in
    match r with
    | Error _ ->
      Log.debug (fun m -> m "follow_cname: cache miss, need to query %a"
                     Domain_name.pp name);
      `Query name, t
    | Ok (`Alias (_, alias), r) ->
      let acc' = Domain_name.Map.add name (Rr_map.singleton Cname (ttl, alias)) acc in
      if Domain_name.Map.mem alias acc then begin
        Log.warn (fun m -> m "follow_cname: cycle detected") ;
        `Out (Rcode.NoError, is_signed r, acc', Name_rr_map.empty), t
      end else begin
        Log.debug (fun m -> m "follow_cname: alias to %a, follow again"
                       Domain_name.pp alias);
        follow t acc' alias
      end
    | Ok (`Entry v, r) ->
      let acc' = Domain_name.Map.add name Rr_map.(singleton typ v) acc in
      Log.debug (fun m -> m "follow_cname: entry found, returning");
      `Out (Rcode.NoError, is_signed r, acc', Name_rr_map.empty), t
    | Ok (`No_domain res, r) ->
      Log.debug (fun m -> m "follow_cname: nodom");
      `Out (Rcode.NXDomain, is_signed r, acc, to_map res), t
    | Ok (`No_data res, r) ->
      Log.debug (fun m -> m "follow_cname: nodata");
      `Out (Rcode.NoError, is_signed r, acc, to_map res), t
    | Ok (`Serv_fail res, r) ->
      Log.debug (fun m -> m "follow_cname: servfail") ;
      `Out (Rcode.ServFail, is_signed r, acc, to_map res), t
  in
  let initial = Name_rr_map.singleton name Cname (ttl, alias) in
  follow t initial alias

let signed_or_nonexisting ~dnssec t ts ty name r =
  if dnssec then
    Option.is_some (is_signed r) || nsec_no t ts ty name || nsec3_covering t ts ty name ||
    upwards_ds_nonexisting t ts name
  else
    true

let ttl k = function
  | Ok (`Entry v, _) -> Rr_map.ttl k v
  | Ok ((`No_data (_, soa), _) | (`No_domain (_, soa), _) | (`Serv_fail (_, soa), _)) ->
    soa.Soa.minimum
  | Ok (`Alias (ttl, _), _) -> ttl
  | Error _ -> 0l

let answer ~dnssec ~dnssec_ok t ts name (typ : Packet.Question.qtype) =
  let packet _t _add ty rcode ~ttl ~rrsig answer authority =
    let answer =
      if dnssec_ok then
        if Domain_name.Map.cardinal answer > 0 then
          match rrsig with
          | Some rrsig -> Name_rr_map.add name Rrsig (ttl, Rr_map.Rrsig_set.singleton rrsig) answer
          | None -> answer
        else
          answer
      else
        answer
    in
    let authority =
      if dnssec_ok then
        if Domain_name.Map.cardinal authority = 1 then
          let name, rr_map = Domain_name.Map.choose authority in
          match Rr_map.find Soa rr_map with
          | None -> authority
          | Some _soa ->
            let authority =
              match rrsig with
              | None -> authority
              | Some rrsig ->
                Name_rr_map.add name Rrsig (ttl, Rr_map.Rrsig_set.singleton rrsig) authority
            in
            match ty with
            | None -> authority
            | Some ty ->
              match find_nsec t ts ty name, find_nsec3 t ts ty name with
              | Some (name, (ttl, nsec), rank), _ ->
                let authority = Name_rr_map.add name Nsec (ttl, nsec) authority in
                (match is_signed rank with
                 | Some rrsig -> Name_rr_map.add name Rrsig (ttl, Rr_map.Rrsig_set.singleton rrsig) authority
                 | None -> authority)
              | _, Some (name, ttl, nsec3, rank) ->
                let authority = Name_rr_map.add name Nsec3 (ttl, nsec3) authority in
                (match is_signed rank with
                 | Some rrsig -> Name_rr_map.add name Rrsig (ttl, Rr_map.Rrsig_set.singleton rrsig) authority
                 | None -> authority)
              | None, _ -> authority
        else
          authority
      else
        authority
    in
    let data = (answer, authority) in
    let flags =
      let f = Packet.Flags.(add `Recursion_available (singleton `Recursion_desired)) in
      if dnssec && match rrsig with Some _ -> true | None -> false then
        Packet.Flags.add `Authentic_data f
      else
        f
    (* XXX: we should look for a fixpoint here ;) *)
    (*    and additional, t = if add then additionals t ts answer else [], t *)
    and data = match rcode with
      | Rcode.NoError -> `Answer data
      | x ->
        let data = if Packet.Answer.is_empty data then None else Some data in
        `Rcode_error (x, Opcode.Query, data)
    in
    flags, data, None
  in
  match typ with
  | `Any ->
    let t, r = Dns_cache.get_any t ts name in
    let ttl = match r with
      | Ok (`No_domain (_, soa), _) -> soa.Soa.minimum
      | Ok (`Entries _rrs, _) -> 0l
      | Error _ -> 0l
    in
    begin match r with
      | Error _e ->
        (* Log.warn (fun m -> m "error %a while looking up %a, query"
                      pp_err e pp_question (name, typ)); *)
        `Query name, t
      | Ok (`No_domain res, r) ->
        Log.debug (fun m -> m "no domain while looking up %a, query" pp_question (name, typ));
        `Packet (packet t false None Rcode.NXDomain ~ttl ~rrsig:(is_signed r) Domain_name.Map.empty (to_map res)), t
      | Ok (`Entries rr_map, r) ->
        Log.debug (fun m -> m "entries while looking up %a" pp_question (name, typ));
        let data = Domain_name.Map.singleton name rr_map in
        `Packet (packet t true None Rcode.NoError ~ttl ~rrsig:(is_signed r) data Domain_name.Map.empty), t
    end
  | `K (Rr_map.K ty) ->
    let t, r = Dns_cache.get_or_cname t ts name ty in
    let ttl = ttl ty r in
    match r with
    | Error _e ->
      (* Log.warn (fun m -> m "error %a while looking up %a, query"
                    _pp_err _e pp_question (name, typ)); *)
      `Query name, t
    | Ok (`No_domain res, r) ->
      if not (signed_or_nonexisting ~dnssec t ts ty name r) then `Query name, t else (
        Log.debug (fun m -> m "no domain while looking up %a" pp_question (name, typ));
        `Packet (packet t false (Some ty) Rcode.NXDomain ~ttl ~rrsig:(is_signed r) Domain_name.Map.empty (to_map res)), t)
    | Ok (`No_data res, r) ->
      if not (signed_or_nonexisting ~dnssec t ts ty name r) then `Query name, t else (
        Log.debug (fun m -> m "no data while looking up %a" pp_question (name, typ));
        `Packet (packet t false (Some ty) Rcode.NoError ~ttl ~rrsig:(is_signed r) Domain_name.Map.empty (to_map res)), t)
    | Ok (`Serv_fail res, r) ->
      if not (signed_or_nonexisting ~dnssec t ts ty name r) then `Query name, t else (
        Log.debug (fun m -> m "serv fail while looking up %a" pp_question (name, typ));
        `Packet (packet t false (Some ty) Rcode.ServFail ~ttl ~rrsig:None Domain_name.Map.empty (to_map res)), t)
    | Ok (`Alias (ttl, alias), r) ->
      if not (signed_or_nonexisting ~dnssec t ts ty name r) then `Query name, t else
      begin
        Log.debug (fun m -> m "alias while looking up %a" pp_question (name, typ));
        match ty with
        | Cname ->
          let data = Name_rr_map.singleton name Cname (ttl, alias) in
          `Packet (packet t false (Some ty) Rcode.NoError ~ttl ~rrsig:(is_signed r) data Domain_name.Map.empty), t
        | ty ->
          match follow_cname t ts ty ~name ttl ~alias with
          | `Out (rcode, rrsig, an, au), t -> `Packet (packet t true (Some ty) rcode ~ttl ~rrsig an au), t
          | `Query n, t -> `Query n, t
      end
    | Ok (`Entry v, r) ->
      if not (signed_or_nonexisting ~dnssec t ts ty name r) then `Query name, t else
        (Log.debug (fun m -> m "entry while looking up %a" pp_question (name, typ));
         let data = Name_rr_map.singleton name ty v in
         `Packet (packet t true (Some ty) Rcode.NoError ~ttl ~rrsig:(is_signed r) data Domain_name.Map.empty), t)

let pick_n rng n xs =
  let l = List.length xs in
  if n >= l then
    xs
  else
    let rec pick amount bound =
      if amount = 0 then
        []
      else
        let e = Randomconv.int ~bound rng in
        let ips'' = pick (amount - 1) (bound - 1) in
        e :: List.map (fun idx -> if idx < e then idx else succ idx) ips''
    in
    let idx = pick n l in
    List.map (List.nth xs) idx

let handle_query t ~qname_minimisation ~dnssec ~dnssec_ok ~rng ip_proto ts (qname, qtype) =
  match answer ~dnssec ~dnssec_ok t ts qname qtype with
  | `Packet (flags, data, additional), t ->
    Log.debug (fun m -> m "handle_query: reply %a (%a)" Domain_name.pp qname
                  Packet.Question.pp_qtype qtype);
    `Reply (flags, data, additional), t
  | `Query name, t ->
    (* DS should be requested at the parent *)
    let name', recover =
      if Domain_name.count_labels name > 1 && qtype = `K (Rr_map.K Ds) then
        let n' = Domain_name.drop_label_exn name in
        n', fun n -> if Domain_name.equal n n' then name else n
      else
        name, Fun.id
    in
    let actions = resolve t ~qname_minimisation ~dnssec ip_proto ts name' qtype in
    let up_to_three = pick_n rng 3 actions in
    let ip1 = 4 - List.length up_to_three in
    let ip2 = max 1 (3 - List.length up_to_three) in
    let _i, queries, t' =
      List.fold_left (fun (i, acc, _t) (zone, name'', types, ips, t) ->
          let name'' = recover name'' in
          let number_of_ips = if i = 0 then ip1 else ip2 in
          let ips = pick_n rng number_of_ips ips in
          Log.debug (fun m -> m "handle_query %a (%a) query %a, resolve zone %a query %a (%a), ips %a"
                        Domain_name.pp qname Packet.Question.pp_qtype qtype
                        Domain_name.pp name Domain_name.pp zone Domain_name.pp name''
                        Fmt.(list ~sep:(any ", ") Packet.Question.pp_qtype) types
                        Fmt.(list ~sep:(any ", ") Ipaddr.pp) ips);
          let actions =
            List.map (fun ip -> (zone, (name'', types), ip)) ips
          in
          succ i, acc @ actions, Some t)
        (0, [], None) up_to_three
    in
    `Queries queries, Option.value ~default:t t'
