/******************************************************************************
 * Copyright (c) 2004, 2008 IBM Corporation
 * All rights reserved.
 * This program and the accompanying materials
 * are made available under the terms of the BSD License
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/bsd-license.php
 *
 * Contributors:
 *     IBM Corporation - initial implementation
 *****************************************************************************/

/*>>>>>>>>>>>>>>>>>>>>> DEFINITIONS & DECLARATIONS <<<<<<<<<<<<<<<<<<<<<<*/

#include <dns.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/socket.h>

#include <ethernet.h>
#include <ipv4.h>
#include <udp.h>

#define DNS_FLAG_MSGTYPE    0xF800	/**< Message type mask (opcode) */
#define DNS_FLAG_SQUERY     0x0000 	/**< Standard query type        */
#define DNS_FLAG_SRESPONSE  0x8000	/**< Standard response type     */
#define DNS_FLAG_RD         0x0100  /**< Recursion desired flag     */
#define DNS_FLAG_RCODE      0x000F	/**< Response code mask
                                         (stores err.cond.) code    */
#define DNS_RCODE_NERROR    0       /**< "No errors" code           */

#define DNS_QTYPE_A         1       /**< A 32-bit IP record type */
#define DNS_QTYPE_CNAME     5       /**< Canonical name record type */

#define DNS_QCLASS_IN       1       /**< Query class for internet msgs */

/** \struct dnshdr
 *  A header for DNS-messages (see RFC 1035, paragraph 4.1.1).
 *  <p>
 *  DNS-message consist of DNS-header and 4 optional sections,
 *  arranged in the following order:<ul>
 *    <li> DNS-header
 *    <li> question section
 *    <li> answer section
 *    <li> authority section
 *    <li> additional section
 *  </ul>
 */
struct dnshdr {
	uint16_t   id;      /**< an identifier used to match up replies */
	uint16_t   flags;   /**< contains op_code, err_code, etc. */
	uint16_t   qdcount; /**< specifies the number of entries in the 
	                         question section */
	uint16_t   ancount; /**< specifies the number of entries in the 
	                         answer section */
	uint16_t   nscount; /**< specifies the number of entries in the
	                         authority section */
	uint16_t   arcount; /**< specifies the number of entries in the 
	                         additional section */
};


/*>>>>>>>>>>>>>>>>>>>>>>>>>>>> PROTOTYPES <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<*/

static void
dns_send_query(int8_t * domain_name);

static void
fill_dnshdr(uint8_t * packet, int8_t * domain_name);

static uint8_t *
dns_extract_name(uint8_t * dnsh, int8_t * head, int8_t * domain_name);

static int8_t
urltohost(char * url, char * host_name);

static int8_t
hosttodomain(char * host_name, char * domain_name);

/*>>>>>>>>>>>>>>>>>>>>>>>>>>> LOCAL VARIABLES <<<<<<<<<<<<<<<<<<<<<<<<<<<*/

static uint8_t ether_packet[ETH_MTU_SIZE];
static int32_t dns_server_ip     = 0;
static int32_t dns_result_ip     = 0;
static int8_t  dns_error         = 0;        /**< Stores error code or 0 */
static int8_t  dns_domain_name[0x100];       /**< Raw domain name        */
static int8_t  dns_domain_cname[0x100];      /**< Canonical domain name  */

/*>>>>>>>>>>>>>>>>>>>>>>>>>>> IMPLEMENTATION <<<<<<<<<<<<<<<<<<<<<<<<<<<<*/

/**
 * DNS: Initialize the environment for DNS client.
 *      To perfrom DNS-queries use the function dns_get_ip.
 *
 * @param  device_socket a socket number used to send and receive packets
 * @param  server_ip     DNS-server IPv4 address (e.g. 127.0.0.1)
 * @return               TRUE in case of successful initialization;
 *                       FALSE in case of fault (e.g. can't obtain MAC).
 * @see                  dns_get_ip
 */
int8_t
dns_init(uint32_t _dns_server_ip) {
	dns_server_ip = _dns_server_ip;
	return 0;
}

/**
 * DNS: For given URL retrieves IPv4 from DNS-server.
 *      <p>
 *      URL can be given in one of the following form: <ul>
 *      <li> scheme with full path with (without) user and password
 *           <br>(e.g. "http://user:pass@www.host.org/url-path");
 *      <li> host name with url-path
 *           <br>(e.g. "www.host.org/url-path");
 *      <li> nothing but host name
 *           <br>(e.g. "www.host.org");
 *      </ul>
 *
 * @param  url       the URL to be resolved
 * @param  domain_ip In case of SUCCESS stores extracted IP.
 *                   In case of FAULT stores zeros (0.0.0.0).
 * @return           TRUE - IP successfuly retrieved;
 *                   FALSE - error condition occurs.
 */
int8_t
dns_get_ip(int8_t * url, uint32_t * domain_ip) {
	/* this counter is used so that we abort after 30 DNS request */
	int32_t i;
	/* this buffer stores host name retrieved from url */
	static int8_t host_name[0x100];

	(* domain_ip) = 0;

	// Retrieve host name from URL
	if (!urltohost((char *) url, (char *) host_name)) {
		printf("\nERROR:\t\t\tBad URL!\n");
		return 0;
	}

	// Reformat host name into a series of labels
	if (!hosttodomain((char *) host_name, (char *) dns_domain_name)) {
		printf("\nERROR:\t\t\tBad host name!\n");
		return 0;
	}

	// Check if DNS server is presented and accessible
	if (dns_server_ip == 0) {
		printf("\nERROR:\t\t\tCan't resolve domain name "
		       "(DNS server is not presented)!\n");
		return 0;
	}

	// Use DNS-server to obtain IP
	dns_result_ip = 0;
	dns_error     = 0;
	strcpy((char *) dns_domain_cname, "");

	for(i = 0; i < 30; ++i) {
		// Use canonical name in case we obtained it
		if (strlen((char *) dns_domain_cname))
		  dns_send_query(dns_domain_cname);
		else
		  dns_send_query(dns_domain_name);

		// setting up a timer with a timeout of one seconds
		set_timer(TICKS_SEC);
		do {
			receive_ether();
			if (dns_error)
				return 0; // FALSE - error
			if (dns_result_ip != 0) {
				(* domain_ip) = dns_result_ip;
				return 1; // TRUE - success (domain IP retrieved)
			}
		} while (get_timer() > 0);
	}

	printf("\nGiving up after %d DNS requests\n", i);
	return 0; // FALSE - domain name wasn't retrieved
}

/**
 * DNS: Handles DNS-messages according to Receive-handle diagram.
 *      Sets dns_result_ip for given dns_domain_name (see dns_get_ip)
 *      or signals error condition occurs during DNS-resolving process
 *      by setting dns_error flag.
 *
 * @param  packet     DNS-packet to be handled
 * @param  packetsize length of the packet
 * @return            ZERO - packet handled successfully;
 *                    NON ZERO - packet was not handled (e.g. bad format)
 * @see               dns_get_ip
 * @see               receive_ether
 * @see               dnshdr
 */
int32_t
handle_dns(uint8_t * packet, int32_t packetsize) {
	struct dnshdr * dnsh = (struct dnshdr *) packet;
	uint8_t * resp_section = packet + sizeof(struct dnshdr);
	/* This string stores domain name from DNS-packets */
	static int8_t handle_domain_name[0x100]; 
	int i;

	// verify ID - is it response for our query?
	if (dnsh -> id != htons(0x1234))
		return 0;

	// Is it DNS response?
	if ((dnsh -> flags & htons(DNS_FLAG_MSGTYPE)) != htons(DNS_FLAG_SRESPONSE))
		return 0;

	// Is error condition occurs? (check error field in incoming packet)
	if ((dnsh -> flags & htons(DNS_FLAG_RCODE)) != DNS_RCODE_NERROR) {
		dns_error = 1;
		return 0;
	}

	/*        Pass all (qdcount) records in question section         */

	for (i = 0; i < htons(dnsh -> qdcount); i++) {
		// pass QNAME
		resp_section = dns_extract_name((uint8_t *) dnsh, (int8_t *) resp_section,
		                                handle_domain_name);
		if (resp_section == NULL) {
			return -1; // incorrect domain name (bad packet)
		}
		// pass QTYPE & QCLASS
		resp_section += 4;
	}

	/*       Handle all (ancount) records in answer section          */

	for (i = 0; i < htons(dnsh -> ancount); i++) {
		// retrieve domain name from the packet
		resp_section = dns_extract_name((uint8_t *) dnsh, (int8_t *) resp_section,
		                                handle_domain_name);

		if (resp_section == NULL) {
			return -1; // incorrect domain name (bad packet)
		}

		// Check the class of the query (should be IN for Internet)
		if (* (uint16_t *) (resp_section + 2) == htons(DNS_QCLASS_IN)) {
			// check if retrieved name fit raw or canonical domain name
			if (!strcmp((char *) handle_domain_name, (char *) dns_domain_name) ||
				!strcmp((char *) handle_domain_name, (char *) dns_domain_cname)) {
				switch (htons(* (uint16_t *) resp_section)) {

				case DNS_QTYPE_A :
					// rdata contains IP
					dns_result_ip = htonl(* (uint32_t *) (resp_section + 10));
					return 0; // IP successfully obtained

				case DNS_QTYPE_CNAME :
					// rdata contains canonical name, store it for further requests
					if (dns_extract_name((uint8_t *) dnsh, (int8_t *) resp_section + 10,
					                     dns_domain_cname) == NULL) {
						// incorrect domain name (bad packet)
						return -1;
					}
					break;
				}
			}
			// continue with next record in answer section
			resp_section += htons(* (uint16_t *) (resp_section + 8)) + 10;
		}
	}
	return 0; // Packet successfully handled but IP wasn't obtained
}

/**
 * DNS: Sends a standard DNS-query (read request package) to a DNS-server.
 *      DNS-server respones with host IP or signals some error condition.
 *      Responses from the server are handled by handle_dns function.
 *
 * @param  domain_name the domain name given as series of labels preceded
 *                     with length(label) and terminated with 0  
 *                     <br>(e.g. "\3,w,w,w,\4,h,o,s,t,\3,o,r,g,\0")
 * @see                handle_dns
 */
static void
dns_send_query(int8_t * domain_name) {
	int qry_len = strlen((char *) domain_name) + 5;

	uint32_t packetsize = sizeof(struct iphdr) +
	                      sizeof(struct udphdr) + sizeof(struct dnshdr) +
	                      qry_len;

	memset(ether_packet, 0, packetsize);
	fill_dnshdr(&ether_packet[
	            sizeof(struct iphdr) + sizeof(struct udphdr)],
	            domain_name);
	fill_udphdr(&ether_packet[
	            sizeof(struct iphdr)], sizeof(struct dnshdr) +
	            sizeof(struct udphdr) + qry_len,
	            UDPPORT_DNSC, UDPPORT_DNSS);
	fill_iphdr(ether_packet,
	           sizeof(struct dnshdr) + sizeof(struct udphdr) +
	           sizeof(struct iphdr) + qry_len,
	           IPTYPE_UDP, 0, dns_server_ip);

	send_ipv4(ether_packet, packetsize);
}

/**
 * DNS: Creates standard DNS-query package. Places DNS-header
 *      and question section in a packet and fills it with
 *      corresponding information.
 *      <p>
 *      Use this function with similar functions for other network layers
 *      (fill_udphdr, fill_iphdr, fill_ethhdr).
 *
 * @param  packet      Points to the place where ARP-header must be placed.
 * @param  domain_name the domain name given as series of labels preceded
 *                     with length(label) and terminated with 0  
 *                     <br>(e.g. "\3,w,w,w,\4,h,o,s,t,\3,o,r,g,\0")
 * @see                fill_udphdr
 * @see                fill_iphdr
 * @see                fill_ethhdr
 */
static void
fill_dnshdr(uint8_t * packet, int8_t * domain_name) {
	struct dnshdr * dnsh = (struct dnshdr *) packet;
	uint8_t * qry_section = packet + sizeof(struct dnshdr);

	dnsh -> id = htons(0x1234);
	dnsh -> flags = htons(DNS_FLAG_SQUERY) | htons(DNS_FLAG_RD);
	dnsh -> qdcount = htons(1);

	strcpy((char *) qry_section, (char *) domain_name);
	qry_section += strlen((char *) domain_name) + 1;

	// fill QTYPE (ask for IP)
	* (uint16_t *) qry_section = htons(DNS_QTYPE_A);
	qry_section += 2;
	// fill QCLASS (IN is a standard class for Internet)
	* (uint16_t *) qry_section = htons(DNS_QCLASS_IN);
}

/**
 * DNS: Extracts domain name from the question or answer section of
 *      the DNS-message. This function is need to support message  
 *      compression requirement (see RFC 1035, paragraph 4.1.4).
 *
 * @param  dnsh        Points at the DNS-header.
 * @param  head        Points at the beginning of the domain_name
 *                     which has to be extracted.
 * @param  domain_name In case of SUCCESS this string stores extracted name.
 *                     In case of FAULT this string is empty.
 * @return             NULL in case of FAULT (domain name > 255 octets); 
 *                     otherwise pointer to the data following the name.
 * @see                dnshdr
 */
static uint8_t *
dns_extract_name(uint8_t * dnsh, int8_t * head, int8_t * domain_name) {
	int8_t * tail = domain_name;
	int8_t * ptr = head;
	int8_t * next_section = NULL;

	while (1) {
		if ((ptr[0] & 0xC0) == 0xC0) {
			// message compressed (reference is used)
			next_section = ptr + 2;
			ptr = (int8_t *) dnsh + (htons(* (uint16_t *) ptr) & 0x3FFF);
			continue;
		}
		if (ptr[0] == 0) {
			// message termination
			tail[0] = 0;
			ptr += 1;
			break;
		}
		// maximum length for domain name is 255 octets w/o termination sym
		if (tail - domain_name + ptr[0] + 1 > 255) {
			strcpy((char *) domain_name, "");
			return NULL;
		}
		memcpy(tail, ptr, ptr[0] + 1);
		tail += ptr[0] + 1;
		ptr += ptr[0] + 1;
	}

	if (next_section == NULL)
		next_section = ptr;

	return (uint8_t *) next_section;
}

/**
 * DNS: Parses URL and returns host name.
 *      Input string can be given as: <ul>
 *      <li> scheme with full path with (without) user and password
 *           <br>(e.g. "http://user:pass@www.host.org/url-path");
 *      <li> host name with url-path
 *           <br>(e.g. "www.host.org/url-path");
 *      <li> nothing but host name
 *           <br>(e.g. "www.host.org");
 *      </ul>
 *
 * @param  url        string that stores incoming URL
 * @param  host_name  In case of SUCCESS this string stores the host name,
 *                    In case of FAULT this string is empty.
 * @return            TRUE - host name retrieved,
 *                    FALSE - host name > 255 octets or empty.
 */
static int8_t
urltohost(char * url, char * host_name) {
	uint16_t length1;
	uint16_t length2;

	strcpy(host_name, "");

	if (strstr(url, "://") != NULL)
		url = strstr(url, "//") + 2;  // URL

	if (strstr(url, "@") != NULL) // truncate user & password
		url = strstr(url, "@") + 1;

	if (strstr(url, "/") != NULL) // truncate url path
		length1 = strstr(url, "/") - url;
	else
		length1 = strlen(url);

	if (strstr(url, ":") != NULL) // truncate port path
		length2 = strstr(url, ":") - url;
	else
		length2 = strlen(url);

	if(length1 > length2)
		length1 = length2;

	if (length1 == 0)
		return 0; // string is empty
	if(length1 >= 256)
		return 0; // host name is too big

	strncpy(host_name, url, length1);
	host_name[length1] = 0;

	return 1; // Host name is retrieved
}

/**
 * DNS: Transforms host name string into a series of labels
 *      each of them preceded with length(label). 0 is a terminator.
 *      "www.domain.dom" -> "\3,w,w,w,\6,d,o,m,a,i,n,\3,c,o,m,\0"
 *      <p>
 *      This format is used in DNS-messages.
 *
 * @param  host_name   incoming string with the host name
 * @param  domain_name resulting string with series of labels
 *                     or empty string in case of FAULT
 * @return             TRUE - host name transformed,
 *                     FALSE - host name > 255 octets or label > 63 octets.
 */
static int8_t
hosttodomain(char * host_name, char * domain_name) {
	char * domain_iter = domain_name;
	char * host_iter   = host_name;

	strcpy(domain_name, "");

	if(strlen(host_name) > 255)
		return 0; // invalid host name (refer to RFC 1035)

	for(; 1; ++host_iter) {
		if(*host_iter != '.' && *host_iter != 0)
			continue;
		*domain_iter = host_iter - host_name;
		if (*domain_iter > 63) {
			strcpy(domain_name, "");
			return 0; // invalid host name (refer to RFC 1035)
		}
		++domain_iter;
		strncpy(domain_iter, host_name, host_iter - host_name);
		domain_iter += (host_iter - host_name);
		if(*host_iter == 0) {
			*domain_iter = 0;
			break;
		}
		host_name = host_iter + 1;
	}
	return 1; // ok
}
