(*
 * Copyright (c) 2005-2006 Tim Deegan <tjd@phlegethon.org>
 * Copyright (c) 2017 Hannes Mehnert <hannes@mehnert.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * dnsserver.ml -- an authoritative DNS server
 *
 *)

let parse buf =
  Dns_zone_state.reset ();
  try
    let buf =
      if String.(get buf (pred (length buf))) = '\n' then buf else buf ^ "\n"
    in
    let lexbuf = Lexing.from_string buf in
    Ok (Dns_zone_parser.zfile Dns_zone_lexer.token lexbuf)
  with
    | Parsing.Parse_error -> Error (`Msg (Fmt.str "zone parse error at line %d" Dns_zone_state.(state.lineno)))
    | Dns_zone_state.Zone_parse_problem s -> Error (`Msg (Fmt.str "zone parse problem at line %d: %s" Dns_zone_state.(state.lineno) s))
    | exn -> Error (`Msg (Printexc.to_string exn))

let src = Logs.Src.create "dns_zone" ~doc:"DNS zone parse"
module Log = (val Logs.src_log src : Logs.LOG)

let decode_zone trie zone data =
  match parse data with
  | Error `Msg msg ->
    Log.warn (fun m -> m "ignoring zone %a: %s (data %s)"
                 Domain_name.pp zone msg data);
    trie, Dns.Name_rr_map.empty
  | Ok rrs ->
    (* we take all resource records within the zone *)
    (* TODO should we add RR one by one, and avoid to add RRs where we have
       delegations? (e.g. a NS ? ; a TXT foo <- the TXT should be discarded) *)
    let in_zone subdomain = Domain_name.is_subdomain ~domain:zone ~subdomain in
    let zone_rrs, other_rrs =
      Domain_name.Map.partition (fun name _ -> in_zone name) rrs
    in
    let trie' = Dns_trie.insert_map zone_rrs trie in
    match Dns_trie.lookup zone Dns.Rr_map.Soa trie', Dns_trie.check trie' with
    | Error _, _ ->
      Log.warn (fun m -> m "ignoring %a: no SOA" Domain_name.pp zone);
      trie, Dns.Name_rr_map.empty
    | _, Error ze ->
      Log.warn (fun m -> m "ignoring %a: zone check failed %a"
                   Domain_name.pp zone Dns_trie.pp_zone_check ze);
      trie, Dns.Name_rr_map.empty
    | Ok _, Ok () -> trie', other_rrs

let add_additional_glue trie (zone, other_rrs) =
  (* collect potential glue:
     - find NS entries for zone
     - find A and AAAA records for name servers in other rrs
       (Dns_trie.check ensures that the NS in zone have an address record)
     - only if the other names are not in zones, they are picked from
       this zone file *)
  match Dns_trie.lookup zone Dns.Rr_map.Ns trie with
  | Error _ ->
    Log.warn (fun m -> m "no NS entries for %a" Domain_name.pp zone);
    trie
  | Ok (_, name_servers) ->
    let not_authoritative nameserver =
      match Dns_trie.lookup nameserver Dns.Rr_map.A trie with
      | Error (`NotAuthoritative | `Delegation _) -> true
      | _ -> false
    in
    let need_glue =
      Domain_name.Host_set.filter not_authoritative name_servers
    in
    let raw_need_glue =
      Domain_name.Host_set.fold (fun ns acc ->
          Domain_name.Set.add (Domain_name.raw ns) acc)
        need_glue Domain_name.Set.empty
    in
    let trie =
      Domain_name.Host_set.fold (fun ns trie ->
          let dn = Domain_name.raw ns in
          match
            Dns.Name_rr_map.find dn Dns.Rr_map.A other_rrs,
            Dns.Name_rr_map.find dn Dns.Rr_map.Aaaa other_rrs
          with
          | Some v4, Some v6 ->
            let trie = Dns_trie.insert ns Dns.Rr_map.A v4 trie in
            Dns_trie.insert ns Dns.Rr_map.Aaaa v6 trie
          | Some v4, None -> Dns_trie.insert ns Dns.Rr_map.A v4 trie
          | None, Some v6 -> Dns_trie.insert ns Dns.Rr_map.Aaaa v6 trie
          | None, None ->
            Log.info (fun m -> m "unknown IP for NS %a (used in zone %a)"
                          Domain_name.pp ns Domain_name.pp zone);
            trie)
        need_glue trie
    in
    Domain_name.Map.iter (fun name value ->
        let leftover =
          if Domain_name.Set.mem name raw_need_glue then
            Dns.Rr_map.remove A (Dns.Rr_map.remove Aaaa value)
          else
            value
        in
        if Dns.Rr_map.is_empty leftover then
          ()
        else begin
          Log.warn (fun m -> m "ignoring %d entries in zone file %a"
                       (Dns.Rr_map.cardinal leftover) Domain_name.pp zone);
          Dns.Rr_map.iter (fun b ->
              Log.warn (fun m -> m "%s" (Dns.Rr_map.text_b name b)))
            leftover
        end)
      other_rrs;
    trie

let decode_keys zone keys =
  match parse keys with
  | Error `Msg msg ->
    Log.warn (fun m -> m "ignoring keys for %a: %s (data: %s)"
                 Domain_name.pp zone msg keys);
    Domain_name.Map.empty
  | Ok rrs ->
    let tst subdomain = Domain_name.is_subdomain ~domain:zone ~subdomain in
    Domain_name.Map.fold (fun n data acc ->
        if not (tst n) then begin
          Log.warn (fun m -> m "ignoring key %a (not in zone %a)"
                       Domain_name.pp n Domain_name.pp zone);
          acc
        end else
          match Dns.Rr_map.(find Dnskey data) with
          | None ->
            Log.warn (fun m -> m "no dnskey found %a" Domain_name.pp n);
            acc
          | Some (_, keys) ->
            match Dns.Rr_map.Dnskey_set.elements keys with
            | [ x ] -> Domain_name.Map.add n x acc
            | xs ->
              Log.warn (fun m -> m "ignoring %d dnskeys for %a (only one supported)"
                           (List.length xs) Domain_name.pp n);
              acc)
      rrs Domain_name.Map.empty

let decode_zones bindings =
  let trie, zones, glue =
    List.fold_left (fun (trie, zones, glues) (name, data) ->
        match Domain_name.of_string name with
        | Error `Msg msg ->
          Log.warn (fun m -> m "ignoring %s, not a domain name %s" name msg);
          trie, zones, glues
        | Ok name ->
          let trie, glue = decode_zone trie name data in
          trie, Domain_name.Set.add name zones, (name, glue) :: glues)
      (Dns_trie.empty, Domain_name.Set.empty, [])
      bindings
  in
  let trie = List.fold_left add_additional_glue trie glue in
  zones, trie

let decode_zones_keys bindings =
  let key_domain = Domain_name.of_string_exn "_keys" in
  let trie, keys, zones, glue =
    List.fold_left (fun (trie, keys, zones, glues) (name, data) ->
        match Domain_name.of_string name with
        | Error `Msg msg ->
          Log.warn (fun m -> m "ignoring %s, not a domain name %s" name msg);
          trie, keys, zones, glues
        | Ok name ->
          if Domain_name.is_subdomain ~domain:key_domain ~subdomain:name then
            let domain = Domain_name.drop_label_exn ~rev:true name in
            let keys' = decode_keys domain data in
            let f key a _b =
              Log.warn (fun m -> m "encountered key %a also in %a"
                           Domain_name.pp key Domain_name.pp domain);
              Some a
            in
            trie, Domain_name.Map.union f keys keys', zones, glues
          else
            let trie, glue = decode_zone trie name data in
            trie, keys, Domain_name.Set.add name zones, (name, glue) :: glues)
      (Dns_trie.empty, Domain_name.Map.empty, Domain_name.Set.empty, [])
      bindings
  in
  let trie = List.fold_left add_additional_glue trie glue in
  zones, trie, Domain_name.Map.bindings keys
