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

open Dns

let create_update zone hostname ip_address =
  let zone = Packet.Question.create zone Soa
  and update =
    let up =
      Domain_name.Map.singleton hostname
        [
          Packet.Update.Remove (Rr_map.K A) ;
          Packet.Update.Add Rr_map.(B (A, (60l, Ipaddr.V4.Set.singleton ip_address)))
        ]
    in
    (Domain_name.Map.empty, up)
  and header = Randomconv.int16 Mirage_crypto_rng.generate, Packet.Flags.empty
  in
  Packet.create header zone (`Update update)

let jump _ serverip port (keyname, zone, dnskey) hostname ip_address =
  Mirage_crypto_rng_unix.use_default ();
  let now = Ptime_clock.now () in
  Logs.app (fun m -> m "updating to %a:%d zone %a A 600 %a %a"
               Ipaddr.pp serverip port
               Domain_name.pp zone
               Domain_name.pp hostname
               Ipaddr.V4.pp ip_address) ;
  Logs.debug (fun m -> m "using key %a: %a" Domain_name.pp keyname Dns.Dnskey.pp dnskey) ;
  let p = create_update zone hostname ip_address in
  match Dns_tsig.encode_and_sign ~proto:`Tcp p now dnskey keyname with
  | Error s ->
    Error (`Msg (Fmt.str "tsig sign error %a" Dns_tsig.pp_s s))
  | Ok (data, mac) ->
    let data_len = String.length data in
    Logs.debug (fun m -> m "built data %d" data_len) ;
    let socket = Dns_cli.connect_tcp serverip port in
    Dns_cli.send_tcp socket data ;
    let read_data = Dns_cli.recv_tcp socket in
    (try (Unix.close socket) with _ -> ()) ;
    match Dns_tsig.decode_and_verify now dnskey keyname ~mac read_data with
    | Error e ->
      Error (`Msg (Fmt.str "nsupdate error %a" Dns_tsig.pp_e e))
    | Ok (reply, _, _) ->
      match Packet.reply_matches_request ~request:p reply with
      | Ok `Update_ack ->
        Logs.app (fun m -> m "successful and signed update!") ;
        Ok ()
      | Ok r ->
        Error (`Msg (Fmt.str "nsupdate expected update ack, received %a" Packet.pp_reply r))
      | Error e ->
        Error (`Msg (Fmt.str "nsupdate error %a (reply %a does not match request %a)"
                       Packet.pp_mismatch e Packet.pp reply Packet.pp p))

open Cmdliner

let serverip =
  let doc = "IP address of DNS server" in
  Arg.(required & pos 0 (some Dns_cli.ip_c) None & info [] ~doc ~docv:"SERVERIP")

let port =
  let doc = "Port to connect to" in
  Arg.(value & opt int 53 & info [ "port" ] ~doc)

let key =
  let doc = "DNS HMAC secret (name:alg:b64key where name is yyy._update.zone)" in
  Arg.(required & pos 1 (some Dns_cli.namekey_c) None & info [] ~doc ~docv:"KEY")

let hostname =
  let doc = "Hostname to modify" in
  Arg.(required & pos 2 (some Dns_cli.domain_name_c) None & info [] ~doc ~docv:"HOSTNAME")

let ipv4_c =
  Arg.conv'
    ((fun s ->
        match Ipaddr.V4.of_string s with
        | Ok ip -> Ok ip
        | Error (`Msg m) -> Error ("failed to parse IP address: " ^ m)),
     Ipaddr.V4.pp)

let ip_address =
  let doc = "New IP address" in
  Arg.(required & pos 3 (some ipv4_c) None & info [] ~doc ~docv:"IP")

let cmd =
  let term =
    Term.(term_result (const jump $ Dns_cli.setup_log $ serverip $ port $ key $ hostname $ ip_address))
  and info = Cmd.info "oupdate" ~version:"10.2.2"
  in
  Cmd.v info term

let () = exit (Cmd.eval cmd)
