/*
 * $Id: smart.c,v 1.3 1999/09/04 05:26:57 morgan Exp morgan $
 *
 * Copyright (c) 1998 Andrew G. Morgan <morgan@linux.kernel.org>
 * All rights reserved.
 */

/*
 * In this process, we run the local X client, that is the user's
 * interface for openning remote connections and also provides an
 * authentication input channel.
 */

#include <unixdefs.h>
#include <dbierrno.h>
#include <dbiapi.h>
#include <gtk/gtk.h>
#include <errno.h>

#include "default.xpm"

#include <security/pam_client.h>

struct internal_state {
    GtkWidget *window;         /* the main window for the widget */
    GtkWidget *text_entry;     /* widget containing username */
    GtkWidget *finger_print;   /* widget containing finger_print pixmap */
    unsigned char *raw_data;   /* raw fingerprint data */
    int count;
    gchar *user;
    int locked;
    pamc_bp_t bp;
    int grabbed_one;
};

#define AGENT_ID  "fp240x240@prototype1"

static void forced_read(int maxlen, unsigned char *raw)
{
    while (maxlen > 0) {

	ssize_t rd = read(fileno(stdin), raw, maxlen);
	if (rd > 0) {

	    raw += rd;
	    maxlen -= rd;

	} else if (rd < 0) {

	    switch (errno) {
	    case EINTR:
		break;
	    default:
		exit(1);
	    }

	} else {

	    exit(1);

	}
    }
}

static void forced_write(int maxlen, const unsigned char *raw)
{
     while (maxlen > 0) {

	ssize_t rd = write(fileno(stdout), raw, maxlen);
	if (rd > 0) {

	    raw += rd;
	    maxlen -= rd;

	} else if (rd < 0) {

	    switch (errno) {
	    case EINTR:
		break;
	    default:
		exit(1);
	    }

	} else {

	    exit(1);

	}
    }
}

/* handling binary prompts */

int read_binary_prompt(pamc_bp_t *bp_p)
{
    unsigned char raw[5], control;
    int length;

    forced_read(5, raw);
    length = PAM_BP_LENGTH(raw);
    control = PAM_BP_CONTROL(raw);
    memset(raw, 0, 5);

    PAM_BP_RENEW(bp_p, control, length);
    forced_read(length, PAM_BP_DATA(*bp_p));

    return 1;
}

void write_binary_prompt(pamc_bp_t bp)
{
    if (isatty(fileno(stdout))) {
	fprintf(stderr, "not outputting binary data to terminal\n");
	exit(1);
    }

    forced_write(PAM_BP_SIZE(bp), (const unsigned char *) bp);
}

/* This is where we cancel the authentication process - this agent is
   happy to do this. */

static gint cancel_event(GtkWidget *button, gpointer data)
{
    struct internal_state *state_info = data;

    PAM_BP_RENEW(&(state_info->bp), PAM_BPC_FAIL, 0);
    write_binary_prompt(state_info->bp);

    gtk_main_quit();

    return TRUE;
}

/* This is where we reply to the server - note, we must totally ignore
   the widget argument */

static gint authenticate_event(GtkWidget *button, gpointer data)
{
    struct internal_state *state_info = data;

    /* construct a reply packet */
    if (state_info->grabbed_one) {

	PAM_BP_RENEW(&state_info->bp, PAM_BPC_DONE,
		     (strlen(state_info->user) + IMAGE_WIDTH*IMAGE_HEIGHT));
	PAM_BP_FILL(state_info->bp, 0, IMAGE_WIDTH*IMAGE_HEIGHT,
		    state_info->raw_data);
	PAM_BP_FILL(state_info->bp, IMAGE_WIDTH*IMAGE_HEIGHT,
		    strlen(state_info->user), state_info->user);

	write_binary_prompt(state_info->bp);
	gtk_main_quit();
	return TRUE;

    } else {

	return FALSE;

    }
}

#define DEFAULT_FPS_BOX_SPACING     10

#define DBI_AutoOT   ((DWORD) 3500)
#define DBI_AutoST   ((DWORD) 3000)

static void open_biomouse()
{
    double length=0;
    BYTE dark_field=0;
    DWORD dwRc;

    dwRc = DBI_InitDriver(&length, &dark_field);
    if (dwRc != DBI_FAST_OK) {
	fprintf(stderr, "failed to initialize biomouse (%d/%d)\n",
		MAJOR_ERROR(dwRc), MINOR_ERROR(dwRc));
	exit(1);
    }

    DBI_DLLInit(length, dark_field);
}

static WORD grab_biomouse_image(BYTE DEW_TYPE *image)
{
    int retval;
    WORD auto_state;

    retval = DBI_GrabPrintState(DBI_AutoOT, DBI_AutoST, &auto_state);
    if ((retval == DBI_FAST_OK) &&
	((auto_state == DBI_PRINT_DETECTED)
	 || (auto_state == DBI_PRINT_STABILIZED))) {
	retval = DBI_GrabImage(image, DBI_AutoOT, DBI_AutoST, &auto_state);
    }

    if (retval != DBI_FAST_OK) {
	fprintf(stderr, "failed to talk to biomouse\n");
	exit(1);
    }

    return auto_state;
}

static void close_biomouse()
{
    DBI_Close();
    DBI_CloseDriver();
}

/* *********************************************
   This is some code to read the fingerprint device
   ********************************************* */

static gchar **raw_to_pixmap(unsigned char *image)
{
    int i=0,line=0;
    gchar **pixmap, *ptr;

    pixmap = g_malloc(((IMAGE_HEIGHT+6)*sizeof(gchar *))
		     + ((IMAGE_WIDTH+4)*IMAGE_HEIGHT)
		     + 300);

    ptr = (gchar *) (pixmap+(IMAGE_HEIGHT+6));
    pixmap[line++] = ptr;
    ptr += 1+sprintf(ptr, "%u %u  4   1", IMAGE_WIDTH, IMAGE_HEIGHT);
    pixmap[line++] = ptr;
    ptr += 1+sprintf(ptr, "  c #400040004000");
    pixmap[line++] = ptr;
    ptr += 1+sprintf(ptr, ". c #800080008000");
    pixmap[line++] = ptr;
    ptr += 1+sprintf(ptr, "o c #C000C000C000");
    pixmap[line++] = ptr;
    ptr += 1+sprintf(ptr, "# c #FFFFFFFFFFFF");

    while ( i<(IMAGE_WIDTH*IMAGE_HEIGHT) ) {
	unsigned c;

	if (! (i % IMAGE_WIDTH) ) {
	    pixmap[line++] = ptr;
	}

	c = image[i];
	if (c < 0xA0) {
	    if (c < 0x60) {
		c = ' ';
	    } else {
		c = '.';
	    }
	} else {
	    if (c < 0xD0) {
		c = 'o';
	    } else {
		c = '#';
	    }
	}

	*ptr++ = (gchar) c;

	if (! (++i%IMAGE_WIDTH)) {
	    *ptr++ = '\0';
	}
    }

    pixmap[line] = NULL;

    /* return the converted image */

    return pixmap;
}

static void render_pixmap(struct internal_state *state_info)
{
    if (state_info) {
	/* convert state_info->raw_data to a pixmap in the
	   state_info->finger_print widget */
	gchar **pixmap;
	GdkPixmap *image;
	GdkBitmap *mask;
	GtkStyle *style;

	pixmap = raw_to_pixmap(state_info->raw_data);
	style = gtk_widget_get_style( state_info->window );
	image = gdk_pixmap_create_from_xpm_d( state_info->window->window,
					      &mask,
					      &style->bg[GTK_STATE_NORMAL],
					      pixmap);
	gtk_pixmap_set(GTK_PIXMAP(state_info->finger_print), image, mask);
	gdk_pixmap_unref(image);
	g_free(pixmap);
    }
}

/* *********************************************
   End of fingerprint reading section
   ********************************************* */

static gint gather_fingerprint(gpointer data)
{
    WORD auto_state;
    struct internal_state *state_info;
    BYTE DEW_TYPE *raw_finger_print;
    gchar *uname;

    state_info = (struct internal_state *) data;

    if (state_info->text_entry) {
	uname = gtk_entry_get_text(GTK_ENTRY(state_info->text_entry));
    } else {
	uname = "<unknown>";
    }

    raw_finger_print = g_malloc0(IMAGE_WIDTH*IMAGE_HEIGHT);
    auto_state = grab_biomouse_image(raw_finger_print);

    if ((auto_state == DBI_PRINT_DETECTED)
	|| (auto_state == DBI_PRINT_STABILIZED)) {
	gchar *raw;

	/* only interested in keeping new print */
	raw = state_info->raw_data;
	state_info->raw_data = raw_finger_print;
	raw_finger_print = raw;

	render_pixmap(state_info);
	state_info->grabbed_one = 1;
    }

    if (raw_finger_print) {
	g_free(raw_finger_print);
    }
    
    if (auto_state == DBI_PRINT_STABILIZED) {
	if (state_info->count++ > 1) {
	    authenticate_event(NULL, state_info);
	}
    } else {
	state_info->count = 0;
    }

    return TRUE;
}

/* this is a callback function. the data arguments are ignored in this
 * example..  More on callbacks below. */

static gint delete_event(GtkWidget *widget, GdkEvent *event, gpointer data)
{
    /* this should terminate the agent */
    return (FALSE);
}

/* another callback */
static void destroy (GtkWidget *widget, gpointer data)
{
    gtk_main_quit ();
}

static int auth_screen(int argc, char **argv,
		       struct internal_state *state_info)
{
    GtkWidget *window, *fps_box;

    /* this is called in all GTK applications.  arguments are parsed from
     * the command line and are returned to the application. */

    gtk_init (&argc, &argv);

    /* create a new window */
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "BioMouse");
    state_info->window = window;

    /* when the window is given the "delete_event" signal (this is given
     * by the window manager, usually by the 'close' option, or on the
     * titlebar), we ask it to call the delete_event () function
     * as defined above.  The data passed to the callback
     * function is NULL and is ignored in the callback function. */
    gtk_signal_connect (GTK_OBJECT (window), "delete_event",
			GTK_SIGNAL_FUNC (delete_event), NULL);
        
    /* here we connect the "destroy" event to a signal handler.  
     * This event occurs when we call gtk_widget_destroy() on the window,
     * or if we return 'FALSE' in the "delete_event" callback. */
    gtk_signal_connect (GTK_OBJECT (window), "destroy",
			GTK_SIGNAL_FUNC (destroy), NULL);
        
    /* sets the border width of the window. */
    gtk_container_border_width (GTK_CONTAINER (window),
				DEFAULT_FPS_BOX_SPACING);

    /* create the fingerprint boxes */
    gtk_widget_set_usize (window, 300, 400);
    gtk_widget_show (window);

    fps_box = gtk_vbox_new (FALSE, DEFAULT_FPS_BOX_SPACING);

    {
	GdkPixmap *image;
	GtkWidget *icon;
	GdkBitmap *mask;
	GtkStyle *style;

	style = gtk_widget_get_style( window );
	image = gdk_pixmap_create_from_xpm_d( window->window,  &mask,
					      &style->bg[GTK_STATE_NORMAL],
					      default_xpm);
	icon = gtk_pixmap_new(image, mask);
	gdk_pixmap_unref(image);
	gtk_box_pack_start(GTK_BOX(fps_box), icon, TRUE, FALSE, 0);
	gtk_widget_show(icon);

	state_info->finger_print = icon;
    }

    /* the username */

    {
	GtkWidget *text_box;
	GtkWidget *text_entry, *user_label;

	text_box = gtk_hbox_new (FALSE, 0);

	user_label = gtk_label_new("Username: ");
	gtk_box_pack_start(GTK_BOX(text_box), user_label, TRUE, TRUE, 0);
	gtk_widget_show(user_label);

	text_entry = gtk_entry_new();
	gtk_entry_set_text(GTK_ENTRY(text_entry), state_info->user);
	if (state_info->locked) {
	    gtk_entry_set_editable(GTK_ENTRY(text_entry), FALSE);
	} else {
	    gtk_entry_select_region(GTK_ENTRY(text_entry), 0,
				    strlen(state_info->user));
	}
	gtk_box_pack_end(GTK_BOX(text_box), text_entry, TRUE, FALSE, 0);
	gtk_widget_show(text_entry);

	gtk_box_pack_start(GTK_BOX(fps_box), text_box, TRUE, FALSE, 0);
	gtk_widget_show(text_box);

	state_info->text_entry = text_entry;
    }
    
    /* buttons */

    {
	GtkWidget *button_box;
	GtkWidget *ok_button, *cancel_button;

	button_box = gtk_hbox_new (TRUE, DEFAULT_FPS_BOX_SPACING);

	ok_button = gtk_button_new_with_label("Authenticate");
	gtk_signal_connect (GTK_OBJECT(ok_button), "clicked",
                            GTK_SIGNAL_FUNC (authenticate_event), state_info);
	gtk_widget_show (ok_button);
	gtk_box_pack_start(GTK_BOX(button_box), ok_button, TRUE, FALSE, 0);

	cancel_button = gtk_button_new_with_label("Cancel");
	gtk_signal_connect (GTK_OBJECT(cancel_button), "clicked",
                            GTK_SIGNAL_FUNC (cancel_event), state_info);
	gtk_widget_show (cancel_button);
	gtk_box_pack_start(GTK_BOX(button_box), cancel_button, TRUE, FALSE, 0);

	gtk_box_pack_start(GTK_BOX(fps_box), button_box, TRUE, FALSE, 0);
	gtk_widget_show (button_box);
    }

    gtk_widget_show(fps_box);
    gtk_container_add (GTK_CONTAINER(window), fps_box);

    /* attempt to grab a fingerprint when and where we can */
    gtk_timeout_add(500, gather_fingerprint, state_info);

    /*
     * set the event loop going...
     */

    gtk_main();

    return 0;
}

void do_conversation(int argc, char **argv, pamc_bp_t bp, int locked)
{
    struct internal_state *state_info;
    unsigned char *user;

    user = 1+sizeof(AGENT_ID)+PAM_BP_DATA(bp);
    state_info =
	(struct internal_state *) g_malloc0(sizeof(struct internal_state));

    state_info->user = g_malloc0(1+strlen(user));
    strcpy(state_info->user, user);

    state_info->locked = locked;
    state_info->bp = bp;
    state_info->grabbed_one = 0;

    open_biomouse();
    auth_screen(argc, argv, state_info);
    close_biomouse();
}

void grab_env(void)
{
    const static char *vars[] = {
	"HOME",    /* this might create a security hole if a user spoofs this */
	"DISPLAY",
	NULL
    };
    pamc_bp_t bp = NULL;
    int i;

    for (i=0; vars[i]; ++i) {

	int length = strlen(vars[i]);

	PAM_BP_RENEW(&bp, PAM_BPC_GETENV, length);
	PAM_BP_FILL(bp, 0, length, vars[i]);
	write_binary_prompt(bp);
	read_binary_prompt(&bp);

	if (PAM_BP_CONTROL(bp) != PAM_BPC_OK) {
	    /* we should fail more gracefully here */
	    exit(1);
	}

	{
	    char *temp = malloc(length + 2 + PAM_BP_LENGTH(bp));
	    sprintf(temp, "%s=%s", vars[i], PAM_BP_DATA(bp));
	    putenv(temp);
	    free(temp);
	}
    }

    PAM_BP_RENEW(&bp, 0, 0);
}

/* the main thing */

int main(int argc, char **argv)
{
    pamc_bp_t bp = NULL;
    int locked;

    /* read the binary prompt that should request a fingerprint */

    if (read_binary_prompt(&bp) != 1) {
	exit(1);
    }

    /* expecting <select>fp240x240@prototype1/<1|0><username> */
    if (PAM_BP_CONTROL(bp) != PAM_BPC_SELECT) {
	exit(1);
    }

    if (PAM_BP_LENGTH(bp) < sizeof(AGENT_ID)) {
	exit(1);
    }

    if (memcmp(PAM_BP_DATA(bp), AGENT_ID, sizeof(AGENT_ID)-1)) {
	exit(1);
    }

    if (PAM_BP_DATA(bp)[sizeof(AGENT_ID)-1] != '/') {
	exit(1);
    }

    locked = PAM_BP_DATA(bp)[sizeof(AGENT_ID)] - '0';
    if ((locked != 1) && (locked != 0)) {
	exit(1);
    }

    grab_env();

    do_conversation(argc, argv, bp, locked);

    exit(0);
}

#ifndef HAVE___BZERO
/*
 * the biomouse library seems to need this on RH 5.2 but not RH 6.0
 * looks like a compiler issue.
 */
void __bzero(void *x, int n)
{
    memset(x, 0, n);
}
#endif /* HAVE___BZERO */
