/* This file demonstrates how to create a BRP server that accepts incoming
 * requests from BRP readers.
 *
 * It's implemented as single threaded console application that runs endlessly,
 * i.e. it can only process 1 reader request at a time.
 * (Real applications will require parallel handling of reader requests,
 * but this is out of scope of this app note.)
 *
 * When a reader connects, the function handle_request() will be called.
 * It does the following:
 *
 *  - Check the connect reason, i.e. the event that caused the reader to
 *    establish a connection.
 *
 *  - Set up a new crypto session.
 *    This is needed if the reader has been (re)booted or the session key has
 *    timed out. To do so, it retrieves the session key from the subdirectory
 *    "certificates_and_keys" (see DEFLATED_HOSTCERT_DIR). This directory
 *    contains the deflated ZIP file generated by BALTECH PKI Certificate Manager.
 *    If no new crypto session is needed, it reuses the existing session key.
 *
 *    Note: In this app note, the session is set to time-out very quickly, i.e
 *    after 60 sec. To change it, see SESSION_EXPIRY_TIMEOUT.
 *
 *  - Handle UDP introspection requests, i.e. configure the reader to always
 *    connect to this server when an event occurs. (For more details, see
 *    docs.baltech.de/udp-intro.html)
 *    To test UDP introspection, use the app note "udp_introspection".
 *
 *  - Handle messages (usually cards presented to the reader).
 */
 
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>


#include <baltech_api.h>


/* This directory contains the certificates/keys required to authenticate to the reader.
 * You can create new keys with BALTECH PKI Certificate Manager. This tool generates
 * a *_HostCertificates.zip file, which contains all required information.
 */
#define DEFLATED_HOSTCERT_DIR    SRC_DIR "/certificates_and_keys/PKIAUTH_BALTECH_DEFAULT"

/* Certificate filenames of host certificate chain
 * (depends on files in DEFLATED_HOSTCERT_DIR) */
#define HOST_CERTCHAIN_FILENAMES { "host_chain/0root.crt", \
                                   "host_chain/1software_ca.crt", \
                                   "host_chain/2host.crt", \
                                   NULL }

/* Certificate filenames of reader certificate chain
 * (depends on files in DEFLATED_HOSTCERT_DIR) */
#define RDR_CERTCHAIN_FILENAMES { "reader_chain/0root.crt", \
                                  "reader_chain/1production_ca.crt", \
                                  NULL }

/* The time in ms after which the reader has to throw away a crypto session key
 * and request a new session key from the server (see CONNECT_REASON_SESSIONKEY_TIMEOUT).
 * If BRP_INFINITE, the reader keeps the session key forever.
 * We recommend you set this value so that the session key will time-out
 * when reader usage is low (e.g. at midnight). */
#define SESSION_EXPIRY_TIMEOUT  (60 * 1000)

/* TCP port number of this server */
#define PORT 2939

/* Maximum number of readers supported (required for session key storage) */
#define MAX_CRYPTO_SESSION_CNT           10

/* Maximum size of settings file, certificate, and key */
#define MAX_FILE_SIZE            0x1000

/* Maximum size of paths */
#define MAX_PATH_LEN             0x100

/* length of the reader's serial number in decimal digits */
#define SNR_LEN    8

/* The reader has booted from a power-up since the last successful connection. */
#define CONNECT_REASON_POWER_UP                           0x0001u

/* An Ethernet cable has been connected to/disconnected from
 * either of the 2 ports since the last successful connection. */
#define CONNECT_REASON_LINK_STATUS_CHANGE                 0x0002u

/* The crypto session has timed out since the last successful connection
 * and needs to be renewed. */
#define CONNECT_REASON_SESSIONKEY_TIMEOUT                 0x0004u

/* A project card has been detected by the reader
 * since the last successful connection. */
#define CONNECT_REASON_CARD_PRESENTATION                  0x0008u

/* The reader has detected a UDP introspection packet since the last
 * successful connection and has thus obtained the host's IP address. */
#define CONNECT_REASON_UDP_INTROSPECTION                  0x0010u

/* The reader has booted from a software reset
 * since the last successful connection. */
#define CONNECT_REASON_RESET                              0x0020u

/* At least 1 connection trial has failed
 * since the last successful connection. */
#define CONNECT_REASON_RECONNECTION_AFTER_FAILED_TRIALS   0x8000u


static struct crypto_session_t {
	long snr;
	unsigned char * session_key;
	size_t session_key_len;
} crypto_sessions[MAX_CRYPTO_SESSION_CNT];
int crypto_session_cnt = 0;


static void failure(char *errormessage)
{
	fprintf(stderr, "%s\n", errormessage);
	exit(EXIT_FAILURE);
}


static void exit_on_error(brp_errcode errcode, char *errormessage,
						  brp_protocol dev)
{
	if (errcode != BRP_OK)
	{
		char * errname = brp_map_errcode(errcode);
		if (errname == NULL)
			fprintf(stderr, "%s [brp_errcode:%08X]\n", errormessage, errcode);
		else
			fprintf(stderr, "%s [%s]\n", errormessage, errname);
		brp_destroy(dev);
		exit(EXIT_FAILURE);
	}
}


static void init_ip_stack(void)
{
#ifdef _WIN32
	WORD wVersionRequested;
	WSADATA wsaData;
	wVersionRequested = MAKEWORD (1, 1);
	if (WSAStartup (wVersionRequested, &wsaData) != 0)
		failure("Failed to initialize Winsock.");
	printf("Winsock has been initialized.\n");
#endif
}


static SOCKET start_listening_on_port(uint16_t port)
{
	struct sockaddr_in server;
	SOCKET sock;

	/* Create socket */
	sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sock < 0)
		failure("Failed to create a socket.");

	/* Create socket address of server */
	memset( &server, 0, sizeof (server));
	/* IPv4 connection */
	server.sin_family = AF_INET;
	/* INADDR_ANY: accept any IP address */
	server.sin_addr.s_addr = htonl(INADDR_ANY);
	/* Port number */
	server.sin_port = htons(port);

	/* Create binding to the server IP address/port */
	if(bind(sock,(struct sockaddr*)&server, sizeof( server)) < 0)
		failure("Failed to bind socket.");

	/* Start listening for client requests on the port of the socket */
	if(listen(sock, 5) == -1 )
		 failure("Failed to start listening.");

	printf("Server is up and running. Waiting for reader(s) to connect ...\n");
	return sock;
}


static SOCKET wait_for_incoming_request(SOCKET sock)
{
	struct sockaddr_in client;
	socklen_t client_len = sizeof(client);
	SOCKET request_sock;
	char client_addr[INET_ADDRSTRLEN];
	
	request_sock = accept(sock, (struct sockaddr*)&client, &client_len);
	if (request_sock < 0)
		failure("Failed to accept incoming request.");
		
	inet_ntop(AF_INET, &client.sin_addr, client_addr, INET_ADDRSTRLEN);
	printf("\nReader with IP address %s has connected. Trying to process request...\n",
		   client_addr);
	
	return request_sock;
}


static uint16_t receive_identification_frame(SOCKET sock, long * snr)
{
	struct {
		uint8_t version;
		uint8_t connect_reasons[sizeof(uint16_t)];
		uint8_t snr[SNR_LEN];
	} identification_frame;
	uint16_t connect_reasons;
	char *end_ptr, snr_null_terminated[SNR_LEN + 1];
	
	if(recv(sock, (char*) &identification_frame, sizeof(identification_frame), 0)
	   < sizeof(identification_frame))
		failure("Reader hasn't sent full identification frame.");
	
	/* Ensure that future reader implementations are recognized. */
	if (identification_frame.version != 1)
		failure("This server implementation only supports identification frame v1.");
	
	/* Convert serial number (snr) from str to int. */
	memcpy(snr_null_terminated, identification_frame.snr, SNR_LEN);
	snr_null_terminated[SNR_LEN] = '\0';
	*snr = strtol(snr_null_terminated, &end_ptr, 10);
	if (*end_ptr != '\0')
		failure("The reader has sent an identification frame with an invalid serial number.");
	
	connect_reasons = identification_frame.connect_reasons[0] << 8 |
			         identification_frame.connect_reasons[1];
	
	printf("  Reader with serial number %08ld has connected. Connect reason: %04X ",
	       *snr, connect_reasons);
	if (connect_reasons & CONNECT_REASON_POWER_UP)
		printf("[Reader power-up]");
	if (connect_reasons & CONNECT_REASON_LINK_STATUS_CHANGE)
		printf("[Ethernet cable connected/disconnected]");
	if (connect_reasons & CONNECT_REASON_SESSIONKEY_TIMEOUT)
		printf("[Session key timeout]");
	if (connect_reasons & CONNECT_REASON_CARD_PRESENTATION)
		printf("[Card presented]");
	if (connect_reasons & CONNECT_REASON_UDP_INTROSPECTION)
		printf("[UDP introspection]");
	if (connect_reasons & CONNECT_REASON_RESET)
		printf("[Reader reset]");
	if (connect_reasons & CONNECT_REASON_RECONNECTION_AFTER_FAILED_TRIALS)
		printf("[Interrupted TCP connection]");
	printf("\n");
	return connect_reasons;
}


void read_file(char *base_dir, char *rel_filename, char *buf, size_t *buf_len)
{
	FILE * handle;
	char path[MAX_PATH_LEN];
	strncpy(path, base_dir, MAX_PATH_LEN-2);
	strcat(path, "/");
	strncat(path, rel_filename, MAX_PATH_LEN - strlen(path) - 1);
	handle = fopen(path, "rt");
	if (handle == NULL)
	{
		printf("Tried to open file %s\n", path);
		failure("Can't find the file.");
	}
	*buf_len = (int) fread(buf, 1, (size_t)MAX_FILE_SIZE, handle);
	fclose(handle);
	if (*buf_len == MAX_FILE_SIZE)
		failure("The file is too big.");
	buf[*buf_len] = '\0';
}


int get_security_level(char * cert_path)
{
	int security_level;
	char * settings_start;
	char settings_buf[MAX_FILE_SIZE];
	size_t settings_buf_len;
	read_file(cert_path, "settings.cfg", settings_buf, &settings_buf_len);
	settings_start = strstr(settings_buf, "\nsecurity_level");
	if (sscanf(settings_start, " security_level = %i", &security_level) != 1)
		failure("Failed to read \"security_level\" from settings.cfg.");
	return security_level;
}


static void setup_new_crypto_session(brp_protocol dev, char * cert_path)
{
	char keypair_buf[MAX_FILE_SIZE], cert_buf[MAX_FILE_SIZE];
	char * host_certchain_filenames[] = HOST_CERTCHAIN_FILENAMES;
	char * rdr_certchain_filenames[] = RDR_CERTCHAIN_FILENAMES;
	char * * cur_cert_filename;
	size_t keypair_buf_len, cert_buf_len;
	brp_errcode errcode;
	
	read_file(cert_path, "keypair.key", keypair_buf, &keypair_buf_len);
	brp_protocol pki = brp_create_pki(
		get_security_level(cert_path),
		/* Device cert chain is added later (see brp_append_dev_ca_certs()) */
		NULL, 0,
		/* Host cert chain is added later (see brp_append_host_certs()) */
		NULL, 0,
		(brp_buf) keypair_buf, keypair_buf_len,
		SESSION_EXPIRY_TIMEOUT);
	/* If brp_create_pki() has failed (i.e. has returned NULL), brp_append_*_certs()
	 * will be skipped and brp_set_crypto() will return an error
	 * => no error checking required here */
	
	/* Key data is sensitive and should not be stored in memory! */
	memset(keypair_buf, 0, (size_t) keypair_buf_len);
	
	/* Add the host certificate chain to the PKI protocol object. */
	cur_cert_filename = host_certchain_filenames;
	for (; *cur_cert_filename != NULL; cur_cert_filename++)
	{
		read_file(cert_path, *cur_cert_filename, cert_buf, &cert_buf_len);
		brp_append_host_certs(pki, (unsigned char*)cert_buf, cert_buf_len);
	}

	/* Add the reader certificate chain to the PKI protocol object. */
	cur_cert_filename = rdr_certchain_filenames;
	for (; *cur_cert_filename != NULL; cur_cert_filename++)
	{
		read_file(cert_path, *cur_cert_filename, cert_buf, &cert_buf_len);
		brp_append_dev_ca_certs(pki, (unsigned char*)cert_buf, cert_buf_len);
	}

	// "pki" will automatically be deleted if this function fails!
	errcode = brp_set_crypto(dev, pki);
	exit_on_error(errcode, "Failed to activate crypto device.", dev);
}


struct crypto_session_t * search_crypto_session_entry(long snr)
{
	int cnt;
	for (cnt = 0; cnt < crypto_session_cnt; cnt++)
		if (crypto_sessions[cnt].snr == snr)
			return &crypto_sessions[cnt];
	return NULL;
}


/**
 * This function saves a PKI session, so it can be reused later.
 *
 * Setting up a PKI session takes a lot of time (> 10 sec), so you'll often
 * want to close a session and reopen it later without the need to
 * re-authenticate.
 * To make this possible, the device's session key remains valid until
 * the session expires (see SESSION_EXPIRY_TIMEOUT).
 */
static void save_crypto_session(brp_protocol dev, long snr)
{
	brp_errcode errcode;
	struct crypto_session_t *cryptsess = search_crypto_session_entry(snr);
	if (cryptsess != NULL)
	{
		if (cryptsess->session_key != NULL)
			free(cryptsess->session_key);
	}
	else
	{
		if (crypto_session_cnt == MAX_CRYPTO_SESSION_CNT)
		{
			brp_destroy(dev);
			failure("You've reached the maximum number of supported crypto sessions.");
		}
		else
		{
			cryptsess = &crypto_sessions[crypto_session_cnt];
			crypto_session_cnt += 1;
			cryptsess->snr = snr;
		}
	}
	
	cryptsess->session_key_len = brp_pki_get_session_buf_size(dev);
	if (cryptsess->session_key_len == 0)
	{
		brp_destroy(dev);
		failure("Failed to get session buffer size.");
	}

	cryptsess->session_key = malloc(cryptsess->session_key_len);
	if (cryptsess->session_key == NULL)
	{
		brp_destroy(dev);
		failure("Failed to allocate session storage memory.");
	}

	// Save session
	errcode = brp_pki_save_session(dev, cryptsess->session_key,
	                               cryptsess->session_key_len, NULL);
	exit_on_error(errcode, "Failed to store session key.", dev);

	printf("  Crypto session saved.\n");
}


static bool restore_crypto_session(brp_protocol dev, long snr)
{
	struct crypto_session_t *cryptsess = search_crypto_session_entry(snr);
	if (cryptsess == NULL)
	{
		printf("  This reader has connected for the first time. "
		       "=> There's no active crypto session yet.\n");
		return false;
	}
	
	brp_errcode errcode = brp_pki_restore_session(dev, cryptsess->session_key,
	                                              cryptsess->session_key_len);
	if (errcode != BRP_OK)
	{
		printf("  Failed to restore session key.\n");
		return false;
	}
	
	free(cryptsess->session_key);
	cryptsess->session_key = NULL;
	
	printf("  Crypto session has been restored.\n");
	return true;
}


/* If you want to assign the reader to a PC (or another network device),
 * connect the PC to the Device port of the reader junction box.
 * You can then retrieve the PC's IP address from the reader.
 */
static void show_device_assigned_to_reader(brp_protocol dev)
{
	brp_buf port2_ip_addr;
	brp_errcode errcode = brp_Eth_GetConnDevIP(dev, &port2_ip_addr, NULL);
	if (errcode == BRP_OK)
		printf("  The following device is connected to the reader's Device port: %d.%d.%d.%d\n",
		       port2_ip_addr[0], port2_ip_addr[1],
		       port2_ip_addr[2], port2_ip_addr[3]);
	else if (errcode == BRP_ERR_STATUS(0x4500, 0x02))
		printf("  No device is connected to the reader's Device port."
		       " => Can't assign the reader to a device.\n");
	else
		exit_on_error(errcode,
				"  Failed to retrieve the device connected to the reader's Device port.", dev);
}


static void config_reader_to_connect_me_on_event(brp_protocol dev,
                                                 char *host_ip_addr,
                                                 uint16_t host_port)
{
	uint16_t host_port_msb;
	brp_errcode errcode;
	
	host_port_msb = htons(host_port);
	errcode = brp_Sys_CfgSetValue(
			dev, 0x0186, 0x20, (brp_buf)&host_port, sizeof(host_port_msb));
	exit_on_error(errcode, "Failed to configure the host port on the reader.", dev);
	
	errcode = brp_Sys_CfgSetValue(
			dev, 0x0186, 0x21,
			(brp_buf) host_ip_addr, (int) strlen(host_ip_addr));
	exit_on_error(errcode, "Failed to configure the host IP address on the reader.", dev);
	
	printf("  The reader has been (re)configured for this host address.\n");
	
	errcode = brp_Sys_Reset(dev);
	exit_on_error(errcode, "Failed to reboot the reader after reconfiguration.",
			dev);
}


static void receive_card_id(brp_protocol dev)
{
	brp_buf msg;
	size_t msg_len;
	brp_MessageType msg_type;
	brp_errcode errcode = brp_AR_GetMessage(dev, &msg_type, &msg,&msg_len,NULL);
	switch (errcode)
	{
		case BRP_OK:
			msg[msg_len] = '\0';
			if (msg_type == 0x00)
				printf("  Card with ID %s has been presented to the reader.\n", msg);
			else
				printf("  Event other than card presentation has been detected (e.g. alarm).\n");
			break;

		case BRP_ERR_AR_NOMESSAGE:
			printf("  No card presentation nor other event detected.\n");
			break;

		case BRP_ERR_AR_DISABLED:
			printf("  Autoread mode is either disabled or the reader isn't "
			       "  configured to autonomously scan for cards.\n");
			break;

		default:
			exit_on_error(errcode,
					"  Failed to get card ID via Autoread (errcode:%08X).\n",
					dev);
			break;
	}
}


static void get_ip_addr_and_port(SOCKET sock, char * ip_addr, uint16_t * port)
{
	socklen_t sa_size = sizeof(struct sockaddr);
	struct sockaddr_in host_sa;
	getsockname(sock, (struct sockaddr *)&host_sa, 	&sa_size);
	inet_ntop(AF_INET, &host_sa.sin_addr, ip_addr, INET_ADDRSTRLEN);
	*port = ntohs(host_sa.sin_port);
}


static void run_brp_communication(SOCKET sock, uint16_t connect_reasons,
                                  long snr)
{
	brp_protocol dev = brp_create();
	brp_errcode errcode;
	if (dev == NULL)
		failure("Failed to create BRP stack.");
	
	errcode = brp_set_io(dev, brp_create_tcpip_by_socket(sock));
	exit_on_error(errcode, "Failed to configure BRP stack to work with TCP.",
			      dev);
	
	setup_new_crypto_session(dev, DEFLATED_HOSTCERT_DIR);
	
	if (connect_reasons & (CONNECT_REASON_POWER_UP |
	                       CONNECT_REASON_SESSIONKEY_TIMEOUT |
	                       CONNECT_REASON_RESET))
		printf("  Negotiating a new crypto session key...\n");
	else
		if (! restore_crypto_session(dev, snr))
			printf("  Renegotiating a crypto session key...\n");
	
	errcode = brp_open(dev);
	exit_on_error(errcode, "Failed to start BRP session.\n", dev);
	
	if (connect_reasons & CONNECT_REASON_UDP_INTROSPECTION)
	{
		char ip_addr[INET_ADDRSTRLEN];
		uint16_t port;
		get_ip_addr_and_port(sock, ip_addr, &port);
		
		show_device_assigned_to_reader(dev);
		
		config_reader_to_connect_me_on_event(dev, ip_addr, port);
	}
	else
	{
		if (connect_reasons & CONNECT_REASON_CARD_PRESENTATION)
			receive_card_id(dev);
		
		save_crypto_session(dev, snr);
	}
	
	brp_destroy(dev);
}


bool stop_server_request(void)
{
	/* Add code here that checks if server is to be stopped. */
	return false;
}


int main( int argc, char *argv[])
{
	SOCKET sock, request_sock;
	long snr;
	
	init_ip_stack();

	sock = start_listening_on_port(PORT);

	do
	{
		uint16_t connect_reasons;
		
		request_sock = wait_for_incoming_request(sock);
		connect_reasons = receive_identification_frame(request_sock, &snr);
		run_brp_communication(request_sock, connect_reasons, snr);
		closesocket(request_sock);
		printf("  Connection closed.\n");
	}
	while (! stop_server_request());
	
	return EXIT_SUCCESS;
}
