/*
 * Copyright 1994 by OpenVision Technologies, Inc.
 * 
 * Permission to use, copy, modify, distribute, and sell this software
 * and its documentation for any purpose is hereby granted without fee,
 * provided that the above copyright notice appears in all copies and
 * that both that copyright notice and this permission notice appear in
 * supporting documentation, and that the name of OpenVision not be used
 * in advertising or publicity pertaining to distribution of the software
 * without specific, written prior permission. OpenVision makes no
 * representations about the suitability of this software for any
 * purpose.  It is provided "as is" without express or implied warranty.
 * 
 * OPENVISION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
 * EVENT SHALL OPENVISION BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
 * USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
 * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 */
/*
 * Copyright (C) 2004,2005 by the Massachusetts Institute of Technology.
 * All rights reserved.
 *
 * Export of this software from the United States of America may
 *   require a specific license from the United States Government.
 *   It is the responsibility of any person or organization contemplating
 *   export to obtain such a license before exporting.
 * 
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of M.I.T. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Furthermore if you modify this software you must label
 * your software as modified software and not distribute it in such a
 * fashion that it might be confused with the original M.I.T. software.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 */

#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
#include <winsock.h>
#else
#include "port-sockets.h"
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdlib.h>
#include <ctype.h>

#include <gssapi/gssapi_generic.h>
#include "gss-misc.h"

#ifdef HAVE_STRING_H
#include <string.h>
#else
#include <strings.h>
#endif

static void usage()
{
     fprintf(stderr, "Usage: gss-server [-port port] [-verbose] [-once]");
#ifdef _WIN32
     fprintf(stderr, " [-threads num]");
#endif
     fprintf(stderr, "\n");
     fprintf(stderr, "       [-inetd] [-export] [-logfile file] service_name\n");
     exit(1);
}

FILE *log;

int verbose = 0;

/*
 * Function: server_acquire_creds
 *
 * Purpose: imports a service name and acquires credentials for it
 *
 * Arguments:
 *
 * 	service_name	(r) the ASCII service name
 * 	server_creds	(w) the GSS-API service credentials
 *
 * Returns: 0 on success, -1 on failure
 *
 * Effects:
 *
 * The service name is imported with gss_import_name, and service
 * credentials are acquired with gss_acquire_cred.  If either opertion
 * fails, an error message is displayed and -1 is returned; otherwise,
 * 0 is returned.
 */
static int server_acquire_creds(service_name, server_creds)
     char *service_name;
     gss_cred_id_t *server_creds;
{
     gss_buffer_desc name_buf;
     gss_name_t server_name;
     OM_uint32 maj_stat, min_stat;

     name_buf.value = service_name;
     name_buf.length = strlen(name_buf.value) + 1;
     maj_stat = gss_import_name(&min_stat, &name_buf, 
				(gss_OID) gss_nt_service_name, &server_name);
     if (maj_stat != GSS_S_COMPLETE) {
	  display_status("importing name", maj_stat, min_stat);
	  return -1;
     }

     maj_stat = gss_acquire_cred(&min_stat, server_name, 0,
				 GSS_C_NULL_OID_SET, GSS_C_ACCEPT,
				 server_creds, NULL, NULL);
     if (maj_stat != GSS_S_COMPLETE) {
	  display_status("acquiring credentials", maj_stat, min_stat);
	  return -1;
     }

     (void) gss_release_name(&min_stat, &server_name);

     return 0;
}

/*
 * Function: server_establish_context
 *
 * Purpose: establishses a GSS-API context as a specified service with
 * an incoming client, and returns the context handle and associated
 * client name
 *
 * Arguments:
 *
 * 	s		(r) an established TCP connection to the client
 * 	service_creds	(r) server credentials, from gss_acquire_cred
 * 	context		(w) the established GSS-API context
 * 	client_name	(w) the client's ASCII name
 *
 * Returns: 0 on success, -1 on failure
 *
 * Effects:
 *
 * Any valid client request is accepted.  If a context is established,
 * its handle is returned in context and the client name is returned
 * in client_name and 0 is returned.  If unsuccessful, an error
 * message is displayed and -1 is returned.
 */
static int server_establish_context(s, server_creds, context, client_name, 
				    ret_flags)
     int s;
     gss_cred_id_t server_creds;
     gss_ctx_id_t *context;
     gss_buffer_t client_name;
     OM_uint32 *ret_flags;
{
     gss_buffer_desc send_tok, recv_tok;
     gss_name_t client;
     gss_OID doid;
     OM_uint32 maj_stat, min_stat, acc_sec_min_stat;
     gss_buffer_desc	oid_name;
     int token_flags;

     if (recv_token(s, &token_flags, &recv_tok) < 0)
       return -1;

     if (recv_tok.value) {
       free (recv_tok.value);
       recv_tok.value = NULL;
     }

     if (! (token_flags & TOKEN_NOOP)) {
       if (log)
	 fprintf(log, "Expected NOOP token, got %d token instead\n",
		 token_flags);
       return -1;
     }

     *context = GSS_C_NO_CONTEXT;

     if (token_flags & TOKEN_CONTEXT_NEXT) {
       do {
	 if (recv_token(s, &token_flags, &recv_tok) < 0)
	   return -1;

	 if (verbose && log) {
	   fprintf(log, "Received token (size=%d): \n", (int) recv_tok.length);
	   print_token(&recv_tok);
	 }

	 maj_stat =
	   gss_accept_sec_context(&acc_sec_min_stat,
				  context,
				  server_creds,
				  &recv_tok,
				  GSS_C_NO_CHANNEL_BINDINGS,
				  &client,
				  &doid,
				  &send_tok,
				  ret_flags,
				  NULL, 	/* ignore time_rec */
				  NULL); 	/* ignore del_cred_handle */

	 if(recv_tok.value) {
	     free(recv_tok.value);
	     recv_tok.value = NULL;
	 }

	 if (send_tok.length != 0) {
	   if (verbose && log) {
	     fprintf(log,
		     "Sending accept_sec_context token (size=%d):\n",
		     (int) send_tok.length);
	     print_token(&send_tok);
	   }
	   if (send_token(s, TOKEN_CONTEXT, &send_tok) < 0) {
	     if (log)
	       fprintf(log, "failure sending token\n");
	     return -1;
	   }

	   (void) gss_release_buffer(&min_stat, &send_tok);
	 }
	 if (maj_stat!=GSS_S_COMPLETE && maj_stat!=GSS_S_CONTINUE_NEEDED) {
	      display_status("accepting context", maj_stat,
			     acc_sec_min_stat);
	      if (*context != GSS_C_NO_CONTEXT)
		      gss_delete_sec_context(&min_stat, context,
					     GSS_C_NO_BUFFER);
	      return -1;
	 }
 
	 if (verbose && log) {
	   if (maj_stat == GSS_S_CONTINUE_NEEDED)
	     fprintf(log, "continue needed...\n");
	   else
	     fprintf(log, "\n");
	   fflush(log);
	 }
       } while (maj_stat == GSS_S_CONTINUE_NEEDED);

       /* display the flags */
       display_ctx_flags(*ret_flags);

       if (verbose && log) {
	 maj_stat = gss_oid_to_str(&min_stat, doid, &oid_name);
	 if (maj_stat != GSS_S_COMPLETE) {
	   display_status("converting oid->string", maj_stat, min_stat);
	   return -1;
	 }
	 fprintf(log, "Accepted connection using mechanism OID %.*s.\n",
		 (int) oid_name.length, (char *) oid_name.value);
	 (void) gss_release_buffer(&min_stat, &oid_name);
       }

       maj_stat = gss_display_name(&min_stat, client, client_name, &doid);
       if (maj_stat != GSS_S_COMPLETE) {
	 display_status("displaying name", maj_stat, min_stat);
	 return -1;
       }
       maj_stat = gss_release_name(&min_stat, &client);
       if (maj_stat != GSS_S_COMPLETE) {
	 display_status("releasing name", maj_stat, min_stat);
	 return -1;
       }
     }
     else {
       client_name->length = *ret_flags = 0;

       if (log)
	 fprintf(log, "Accepted unauthenticated connection.\n");
     }

     return 0;
}

/*
 * Function: create_socket
 *
 * Purpose: Opens a listening TCP socket.
 *
 * Arguments:
 *
 * 	port		(r) the port number on which to listen
 *
 * Returns: the listening socket file descriptor, or -1 on failure
 *
 * Effects:
 *
 * A listening socket on the specified port and created and returned.
 * On error, an error message is displayed and -1 is returned.
 */
static int create_socket(port)
     u_short port;
{
     struct sockaddr_in saddr;
     int s;
     int on = 1;
     
     saddr.sin_family = AF_INET;
     saddr.sin_port = htons(port);
     saddr.sin_addr.s_addr = INADDR_ANY;

     if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
	  perror("creating socket");
	  return -1;
     }
     /* Let the socket be reused right away */
     (void) setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));
     if (bind(s, (struct sockaddr *) &saddr, sizeof(saddr)) < 0) {
	  perror("binding socket");
	  (void) close(s);
	  return -1;
     }
     if (listen(s, 5) < 0) {
	  perror("listening on socket");
	  (void) close(s);
	  return -1;
     }
     return s;
}

static float timeval_subtract(tv1, tv2)
	struct timeval *tv1, *tv2;
{
	return ((tv1->tv_sec - tv2->tv_sec) +
		((float) (tv1->tv_usec - tv2->tv_usec)) / 1000000);
}

/*
 * Yes, yes, this isn't the best place for doing this test.
 * DO NOT REMOVE THIS UNTIL A BETTER TEST HAS BEEN WRITTEN, THOUGH.
 * 					-TYT
 */
static int test_import_export_context(context)
	gss_ctx_id_t *context;
{
	OM_uint32	min_stat, maj_stat;
	gss_buffer_desc context_token, copied_token;
	struct timeval tm1, tm2;
	
	/*
	 * Attempt to save and then restore the context.
	 */
	gettimeofday(&tm1, (struct timezone *)0);
	maj_stat = gss_export_sec_context(&min_stat, context, &context_token);
	if (maj_stat != GSS_S_COMPLETE) {
		display_status("exporting context", maj_stat, min_stat);
		return 1;
	}
	gettimeofday(&tm2, (struct timezone *)0);
	if (verbose && log)
		fprintf(log, "Exported context: %d bytes, %7.4f seconds\n",
			(int) context_token.length, 
			timeval_subtract(&tm2, &tm1));
	copied_token.length = context_token.length;
	copied_token.value = malloc(context_token.length);
	if (copied_token.value == 0) {
	  if (log)
	    fprintf(log, "Couldn't allocate memory to copy context token.\n");
	  return 1;
	}
	memcpy(copied_token.value, context_token.value, copied_token.length);
	maj_stat = gss_import_sec_context(&min_stat, &copied_token, context);
	if (maj_stat != GSS_S_COMPLETE) {
		display_status("importing context", maj_stat, min_stat);
		return 1;
	}
	free(copied_token.value);
	gettimeofday(&tm1, (struct timezone *)0);
	if (verbose && log)
		fprintf(log, "Importing context: %7.4f seconds\n",
			timeval_subtract(&tm1, &tm2));
	(void) gss_release_buffer(&min_stat, &context_token);
	return 0;
}

/*
 * Function: sign_server
 *
 * Purpose: Performs the "sign" service.
 *
 * Arguments:
 *
 * 	s		(r) a TCP socket on which a connection has been
 *			accept()ed
 * 	service_name	(r) the ASCII name of the GSS-API service to
 * 			establish a context as
 *	export		(r) whether to test context exporting
 * 
 * Returns: -1 on error
 *
 * Effects:
 *
 * sign_server establishes a context, and performs a single sign request.
 *
 * A sign request is a single GSS-API sealed token.  The token is
 * unsealed and a signature block, produced with gss_sign, is returned
 * to the sender.  The context is the destroyed and the connection
 * closed.
 *
 * If any error occurs, -1 is returned.
 */
static int sign_server(s, server_creds, export)
     int s;
     gss_cred_id_t server_creds;
     int export;
{
    gss_buffer_desc client_name, xmit_buf, msg_buf;
    gss_ctx_id_t context;
    OM_uint32 maj_stat, min_stat;
    int i, conf_state, ret_flags;
    char	*cp;
    int token_flags;

    /* Establish a context with the client */
    if (server_establish_context(s, server_creds, &context,
                                  &client_name, &ret_flags) < 0)
        return(-1);

    if (context == GSS_C_NO_CONTEXT) {
        printf("Accepted unauthenticated connection.\n");
    }
    else {
        printf("Accepted connection: \"%.*s\"\n",
                (int) client_name.length, (char *) client_name.value);
        (void) gss_release_buffer(&min_stat, &client_name);

        if (export) {
            for (i=0; i < 3; i++)
                if (test_import_export_context(&context))
                    return -1;
        }
    }

    do {
        /* Receive the message token */
        if (recv_token(s, &token_flags, &xmit_buf) < 0)
            return(-1);

        if (token_flags & TOKEN_NOOP) {
            if (log)
                fprintf(log, "NOOP token\n");
            if(xmit_buf.value) {
                free(xmit_buf.value);
                xmit_buf.value = 0;
            }
            break;
        }

        if (verbose && log) {
            fprintf(log, "Message token (flags=%d):\n", token_flags);
            print_token(&xmit_buf);
        }

        if ((context == GSS_C_NO_CONTEXT) &&
             (    token_flags & (TOKEN_WRAPPED|TOKEN_ENCRYPTED|TOKEN_SEND_MIC))) {
            if (log)
                fprintf(log,
                         "Unauthenticated client requested authenticated services!\n");
            if(xmit_buf.value) {
                free (xmit_buf.value);
                xmit_buf.value = 0;
            }
            return(-1);
        }

        if (token_flags & TOKEN_WRAPPED) {
            maj_stat = gss_unwrap(&min_stat, context, &xmit_buf, &msg_buf,
                                   &conf_state, (gss_qop_t *) NULL);
            if (maj_stat != GSS_S_COMPLETE) {
                display_status("unsealing message", maj_stat, min_stat);
                if(xmit_buf.value) {
                    free (xmit_buf.value);
                    xmit_buf.value = 0;
                }
                return(-1);
            } else if (! conf_state && (token_flags & TOKEN_ENCRYPTED)) {
                fprintf(stderr, "Warning!  Message not encrypted.\n");
            }

            if(xmit_buf.value) {
                free (xmit_buf.value);
                xmit_buf.value = 0;
            }
        }
        else {
            msg_buf = xmit_buf;
        }

        if (log) {
            fprintf(log, "Received message: ");
            cp = msg_buf.value;
            if ((isprint((int) cp[0]) || isspace((int) cp[0])) &&
                 (isprint((int) cp[1]) || isspace((int) cp[1]))) {
                fprintf(log, "\"%.*s\"\n", (int) msg_buf.length, 
                         (char *) msg_buf.value);
                 } else {
                     fprintf(log, "\n");
                     print_token(&msg_buf);
                 }
        }

        if (token_flags & TOKEN_SEND_MIC) {
            /* Produce a signature block for the message */
            maj_stat = gss_get_mic(&min_stat, context, GSS_C_QOP_DEFAULT,
                                    &msg_buf, &xmit_buf);
            if (maj_stat != GSS_S_COMPLETE) {
                display_status("signing message", maj_stat, min_stat);
                return(-1);
            }

            if(msg_buf.value) {
                free (msg_buf.value);
                msg_buf.value = 0;
            }

            /* Send the signature block to the client */
            if (send_token(s, TOKEN_MIC, &xmit_buf) < 0)
                return(-1);

            if(xmit_buf.value) {
                free (xmit_buf.value);
                xmit_buf.value = 0;
            }
        }
        else {
            if(msg_buf.value) {
                free (msg_buf.value);
                msg_buf.value = 0;
            }
            if (send_token(s, TOKEN_NOOP, empty_token) < 0)
                return(-1);
        }
    } while (1 /* loop will break if NOOP received */);

    if (context != GSS_C_NO_CONTEXT) {
        /* Delete context */
        maj_stat = gss_delete_sec_context(&min_stat, &context, NULL);
        if (maj_stat != GSS_S_COMPLETE) {
            display_status("deleting context", maj_stat, min_stat);
            return(-1);
        }
    }

    if (log)
        fflush(log);

    return(0);
}

static int max_threads = 1;

#ifdef _WIN32
static thread_count = 0;
static HANDLE hMutex = NULL;
static HANDLE hEvent = NULL;

void
InitHandles(void)
{
    hMutex = CreateMutex(NULL, FALSE, NULL);
    hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
}

void
CleanupHandles(void)
{
    CloseHandle(hMutex);
    CloseHandle(hEvent);
}

BOOL
WaitAndIncrementThreadCounter(void)
{
    for (;;) {
        if (WaitForSingleObject(hMutex, INFINITE) == WAIT_OBJECT_0) {
            if ( thread_count < max_threads ) {
                thread_count++;
                ReleaseMutex(hMutex);
                return TRUE;
            } else {
                ReleaseMutex(hMutex);

                if (WaitForSingleObject(hEvent, INFINITE) == WAIT_OBJECT_0) {
                    continue;
                } else {
                    return FALSE;
                }
            }
        } else {
            return FALSE;
        }
    }
}

BOOL
DecrementAndSignalThreadCounter(void)
{
    if (WaitForSingleObject(hMutex, INFINITE) == WAIT_OBJECT_0) {
        if ( thread_count == max_threads )
            ResetEvent(hEvent);
        thread_count--;
        ReleaseMutex(hMutex);
        return TRUE;
    } else {
        return FALSE;
    }
}
#endif

struct _work_plan {
    int             s;
    gss_cred_id_t   server_creds;
    int             export;
};

void 
worker_bee(void * param)
{
    struct _work_plan *work = (struct _work_plan *) param;

    /* this return value is not checked, because there's
     * not really anything to do if it fails 
     */
    sign_server(work->s, work->server_creds, work->export);
    closesocket(work->s);
    free(work);

#ifdef _WIN32
    if ( max_threads > 1 )
        DecrementAndSignalThreadCounter();
#endif
}

int
main(argc, argv)
     int argc;
     char **argv;
{
     char *service_name;
     gss_cred_id_t server_creds;
     OM_uint32 min_stat;
     u_short port = 4444;
     int once = 0;
     int do_inetd = 0;
     int export = 0;

     log = stdout;
     display_file = stdout;
     argc--; argv++;
     while (argc) {
	  if (strcmp(*argv, "-port") == 0) {
	       argc--; argv++;
	       if (!argc) usage();
	       port = atoi(*argv);
	  } 
#ifdef _WIN32
          else if (strcmp(*argv, "-threads") == 0) {
              argc--; argv++;
              if (!argc) usage();
              max_threads = atoi(*argv);
          } 
#endif
          else if (strcmp(*argv, "-verbose") == 0) {
	      verbose = 1;
	  } else if (strcmp(*argv, "-once") == 0) {
	      once = 1;
	  } else if (strcmp(*argv, "-inetd") == 0) {
	      do_inetd = 1;
	  } else if (strcmp(*argv, "-export") == 0) {
	      export = 1;
	  } else if (strcmp(*argv, "-logfile") == 0) {
	      argc--; argv++;
	      if (!argc) usage();
	      /* Gross hack, but it makes it unnecessary to add an
                 extra argument to disable logging, and makes the code
                 more efficient because it doesn't actually write data
                 to /dev/null. */
	      if (! strcmp(*argv, "/dev/null")) {
		log = display_file = NULL;
	      }
	      else {
		log = fopen(*argv, "a");
		display_file = log;
		if (!log) {
		  perror(*argv);
		  exit(1);
		}
	      }
	  } else
	       break;
	  argc--; argv++;
     }
     if (argc != 1)
	  usage();

     if ((*argv)[0] == '-')
	  usage();

#ifdef _WIN32
    if (max_threads < 1) {
        fprintf(stderr, "warning: there must be at least one thread\n");
        max_threads = 1;
    }

    if (max_threads > 1 && do_inetd)
        fprintf(stderr, "warning: one thread may be used in conjunction with inetd\n");

    InitHandles();
#endif

     service_name = *argv;

     if (server_acquire_creds(service_name, &server_creds) < 0)
	 return -1;
     
     if (do_inetd) {
	 close(1);
	 close(2);

	 sign_server(0, server_creds, export);
	 close(0);
     } else {
	 int stmp;

 	 if ((stmp = create_socket(port)) >= 0) {
             if (listen(stmp, max_threads == 1 ? 0 : max_threads) < 0)
                 perror("listening on socket");

 	     do {
                 struct _work_plan * work = malloc(sizeof(struct _work_plan));

                 if ( work == NULL ) {
                     fprintf(stderr, "fatal error: out of memory");
                     break;
                 }

 		 /* Accept a TCP connection */
                 if ((work->s = accept(stmp, NULL, 0)) < 0) {
 		     perror("accepting connection");
 		     continue;
 		 }
                  
                 work->server_creds = server_creds;
                 work->export = export;

                 if (max_threads == 1) {
                     worker_bee((void *)work);
                 } 
#ifdef _WIN32
                 else {
                     if ( WaitAndIncrementThreadCounter() ) {
                         uintptr_t handle = _beginthread(worker_bee, 0, (void *)work);
                         if (handle == (uintptr_t)-1) {
                             closesocket(work->s);
                             free(work);
                         }
                     } else {
                         fprintf(stderr, "fatal error incrementing thread counter");
                         closesocket(work->s);
                         free(work);
                         break;
                     }
                 }
#endif
 	     } while (!once);
 
 	     closesocket(stmp);
 	 }
     }

     (void) gss_release_cred(&min_stat, &server_creds);

#ifdef _WIN32
    CleanupHandles();
#endif

     return 0;
}
