open Unix;;
open Printf;;

(** Création et attachement d'une prise UDP *)
let open_udp_server port =
  let socket = socket PF_INET SOCK_DGRAM 0 in
  bind socket (ADDR_INET (inet_addr_anyport));
  socket;;

(** Attente de l'acquittement du paquet de numéro n *)
let rec wait_for_ack socket buffer n =
  match read socket buffer 0 4 with
    4 when (buffer.[0] = Char.chr 0 &&
            buffer.[1] = Char.chr 4 &&
            buffer.[2] = Char.chr (n / 256) &&
            buffer.[3] = Char.chr (n mod 256)) -> ()
  | 4 when (buffer.[0] = Char.chr 0 &&
            buffer.[1] = Char.chr 5) ->
             raise (Failure "Error datagram received")
  | _ ->
      prerr_endline "Bad packet received!\n";
      wait_for_ack socket buffer n;;

(** Émission d'un datagramme d'erreur *)
let send_error socket dest code msg =
  let packet = sprintf "\000\005\000%c%s\000" code msg in
  let packet_length = (String.length packetin
  ignore (sendto socket packet 0 packet_length [] dest);;

(** Création d'une prise UDP et pseudo-connexion *)
let connect_udp_client dest =
  let socket = socket PF_INET SOCK_DGRAM 0 in
  setsockopt_float socket SO_RCVTIMEO 1.;
  connect socket dest;
  socket;;


let retry_nb = 5;;
(** Envoi de paquet avec attente d'acquittement *)
 let rec send_with_ack socket packet packet_size n ack_buffer =
   let rec retry i =
     if i = 0 then raise (Failure "Timeout, maximum retry reached")
     else
       try
         ignore (write socket packet 0 packet_size);
         wait_for_ack socket ack_buffer n
       with Unix_error(EAGAIN,"read",_) -> retry (i-1)
   in
   retry retry_nb;;

(** Lecture du maximum de données *)
let max_read fd buffer start length =
  let rec loop from nb  =
    if length <= 0 then 0 else
    match read fd buffer from nb with
      0 -> from - start
    | r -> loop (from + r) (nb - rin
  loop start length;;

let unwind_protect f x g y = try let v = f x in g yv with e -> g yraise e;;

(** Envoi du fichier *)
let block_size = 512
let header_size = 4
let send_file dest name =
  let fd = openfile name [O_RDONLY] 0 in
  let socket = try connect_udp_client dest with e -> close fdraise e in
  let packet = String.create (header_size + block_sizein
  packet.[0] <- Char.chr 0;
  packet.[1] <- Char.chr 3;
  let ack_buffer = String.create 4 in
  let rec send i =
    packet.[2] <- Char.chr (i/256);
    packet.[3] <- Char.chr (i mod 256);
    let len = max_read fd packet header_size block_size in
    send_with_ack socket packet (header_size + leni ack_buffer;
    if len = block_size then send (succ i)
  in
  let close_everything () =
    close fd;
    close socket
  in
  unwind_protect send 1 close_everything ();;

(** Extraction du nom et du mode trouvé dans la requête *)
let extract_name_and_mode buffer len =
  try
    let name_offset = 2 in
    let name_len = String.index_from buffer 2 '\000' - name_offset in
    let name = String.sub buffer name_offset name_len in
    let mode_offset = name_offset + name_len + 1 in
    let mode = String.sub buffer mode_offset (len - mode_offset - 1) in
    name,mode
  with _ -> raise (Failure "extract_name_and_mode");;

(** Traitement d'une requête de lecture *)
let process_read_request socket dest buffer len =
  try
    match extract_name_and_mode buffer len with
      name"octet" ->
        begin
          eprintf "Sending file %s\n" name;
          flush Pervasives.stderr;
          try
            send_file dest name
          with
            Unix_error (ENOENT,"open",_) ->
              prerr_endline "File not found";
              send_error socket dest '\001' "File not found"
          | Unix_error (EACCES,"open",_) ->
              prerr_endline "Access violation";
              send_error socket dest '\002' "Access violation"
          | Unix_error (ECONNREFUSED,_,_) ->
              prerr_endline "Peer socket closed"
          | e ->
              prerr_endline (Printexc.to_string e);
              send_error socket dest '\000' "Unexpected error"
        end
    | _ ->
        prerr_endline "Mode not supported";
        send_error socket dest '\004' "Mode not supported"
  with Failure "extract_name_and_mode" ->
    send_error socket dest '\000' "Bad request";;

(** Lance un serveur TFTP sur le port passé en argument *)
let establish_tftp_server port =
  let socket = open_udp_server port in
  let buffer_size = 1024 in
  let buffer = String.create buffer_size in
  while true do
    let len,from = recvfrom  socket buffer 0 buffer_size [] in
    match Char.code buffer.[0], Char.code buffer.[1] with
    | 0,1 -> (* Read *)
        process_read_request socket from buffer len
    | 0,2 -> (* Write *)
        prerr_endline "Operation not supported" ;
        send_error socket from '\004' "Operation not supported"
    | _,i -> (* Other *)
        eprintf "Packet with opcode %d received\n" i;
        flush Pervasives.stderr
  done;;

(** Récupère un numéro de port à partir d'une chaîne de caractères *)
let port_of_string port =
  try
    try (getservbyname port "tcp").s_port
    with Not_found -> int_of_string port
  with _ -> raise (Failure "port_of_string");;

(** fonction principale *)
let main () =
  if Array.length Sys.argv <> 2 then
    begin
      prerr_endline ("Usage: "^Sys.argv.(0)^" port");
      exit 1
    end
  else
    try
      let port = port_of_string Sys.argv.(1) in
      establish_tftp_server port
    with
      Failure "port_of_string" ->
        prerr_endline ("Unknown port: "^Sys.argv.(1));
        exit 3;;

handle_unix_error main ();;