/* this module identifies a user based on the remote owner of a socket
 * It is designed for use with Linux and makes extensive use of the
 * /proc/ filesystem.
 *
 * See end for license information.
 *
 * Written by Andrew G. Morgan <morgan@linux.kernel.org>
 */

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <stdarg.h>
#include <syslog.h>
#include <stdlib.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <pwd.h>

#define PAM_SM_AUTH
#include <security/pam_modules.h>
#include <security/_pam_macros.h>

#ifndef PAM_EXTERN
#define PAM_EXTERN
#endif

#define BLEN 300  /* this needs to be longer than any line from /proc/XXX/ */

/* this is the 'expected' os type and corresponds to a type that will
   provide a straightforward and locally understood username.  It can
   be overridden with a command line argument. */

#define OSTYPE "UNIX"
#define IDENTD_PORT  113           /* port to probe on remote hosts */
#define TIMEOUT      15            /* seconds */
#define PROBE_FD     0             /* file descriptor of local port */

/* options */

#define NETID_SETUSER    0x0001    /* explicitly set the PAM_USER */
#define NETID_AUTHUSER   0x0002    /* based on PAM_USER pass or fail */
#define NETID_ENFORCE    0x0004    /* never return PAM_IGNORE -- fail */
#define NETID_NOLOGERR   0x0008
#define NETID_NOLOG      0x0010
#define NETID_REFRESH    0x0020
#define NETID_DEBUG      0x0040
#define NETID_NONET      0x0080    /* fail if the user is connected to
				      the network.. (can be overridden with
				      'auth') */

/* some "syslogging" */

#ifdef PROG
static int ctrl;
#undef D
#define D(x) if (ctrl & NETID_DEBUG) { printf x ; printf("\n"); }
#endif /* PROG */

static void _pam_log(int err, const char *format, ...)
{
    va_list args;

    va_start(args, format);
#ifdef PROG
    fprintf(stderr, "netid[%d]: ", err);
    vfprintf(stderr, format, args);
    fprintf(stderr, "\n");
#else /* PROG */
    openlog("PAM-netid", LOG_CONS|LOG_PID, LOG_AUTH);
    vsyslog(err, format, args);
#endif /* PROG */
    va_end(args);
    closelog();
}

static int _pam_parse(int *timeout_p, int *rport_p, const char **ostype_p,
		      int *probe_fd_p,
#ifdef PROG
		      int *pid_p,
#endif /* PROG */
		      int argc, const char **argv)
{
     int ctl=0;

     /* reset timeout and remote port */

     *timeout_p = TIMEOUT;
     *rport_p = IDENTD_PORT;
     *ostype_p = OSTYPE;
     *probe_fd_p = PROBE_FD;

#ifdef PROG
     *pid_p = (int) getpid();
#endif /* PROG */

     /* step through arguments */
     for (ctl=0; argc-- > 0; ++argv) {
	 char *ep = NULL;

	 /* generic options */

	 if (!strcmp(*argv,"debug")) {
	     ctl |= NETID_DEBUG;
	 } else if (!strcmp(*argv,"setuser")) {
	     ctl |= NETID_SETUSER;
	 } else if (!strcmp(*argv,"auth")) {
	     ctl |= NETID_AUTHUSER;
	 } else if (!strcmp(*argv,"enforce")) {
	     ctl |= NETID_ENFORCE;
	 } else if (!strcmp(*argv,"quiet")) {
	     ctl |= NETID_NOLOGERR;
	 } else if (!strcmp(*argv,"silent")) {
	     ctl |= NETID_NOLOGERR|NETID_NOLOG;
	 } else if (!strcmp(*argv,"refresh")) {
	     ctl |= NETID_REFRESH;
	 } else if (!strcmp(*argv,"nonet")) {
	     ctl |= NETID_NONET;
	 } else if (!strncmp(*argv,"timeout=",8)) {
	     if ((*timeout_p = atoi(*argv+8)) < 0) {
		 *timeout_p = TIMEOUT;
	     }
	 } else if (!strncmp(*argv,"port=",5)) {
	     if ((*rport_p = atoi(*argv+5)) < 0) {
		 *rport_p = IDENTD_PORT;
	     }
	 } else if (!strncmp(*argv,"os=",3)) {
	     *ostype_p = *argv+3;
	 } else if (!strncmp(*argv,"fd=",3)) {
	     if ((*probe_fd_p = atoi(*argv+3)) < 0) {
		 *probe_fd_p = PROBE_FD;
	     }
#ifdef PROG
	 } else if (!strncmp(*argv,"pid=",4)) {
	     *pid_p = atoi(*argv+4);
#endif /* PROG */
	 } else {
	     _pam_log(LOG_ERR,"pam_parse: unknown option; %s",*argv);
	 }
     }

     return ctl;
}

static int field(const char *buf, int fnum)
{
    const char *text = buf;

    goto loop;

    while (--fnum > 0) {
	while (*text && !isspace(*text)) {
	    ++text;
	}
    loop:
	while (*text && isspace(*text)) {
	    ++text;
	}
    }

    return (text - buf);
}

static __u32 atoh(const char *hexstring)
{
    int value=0;

    while (*hexstring && !isspace(*hexstring)) {
	if (isdigit(*hexstring)) {
	    value = 16*value+ *hexstring - '0';
	} else if (isalpha(*hexstring)) {
	    if (*hexstring < 'G') {
		value = 16*value+ *hexstring + 10 - 'A';
	    } else if (*hexstring > 'f' || *hexstring < 'a') {
		break;
	    } else {
		value = 16*value+ *hexstring + 10 - 'a';
	    }
	} else {
	    break;
	}

	++hexstring;
    }

    return value;
}

/* this function reads the contents of a symbolic link as an integer */

static int read_link(const char *fmt, ... )
{
    int len, val=-1;
    char buffer[BLEN];
    char file[BLEN];
    va_list ap;
    
    va_start(ap, fmt);
    if (-1 == vsnprintf(file, BLEN, fmt, ap)) {
	return val;
    }
    va_end(ap);

    len = readlink(file, buffer, BLEN);
    if (len <= 0) {
	D(("no access to [%s]: %d", file, len));
	return val;
    }

    if (len < BLEN) {
	buffer[len] = '\0';
	if (!strncmp("[0000]:", buffer, 7)) {
	    val = atoi(buffer+7);
	} else if (!strncmp("socket:[", buffer, 8)) {
	    val = atoi(buffer+8);
	}
    }

    return val;
}

/* this function looks for a number in the proc filesystem */

static char *find_value(const char *filename, int field_number,
			const char *token)
{
    char buffer[BLEN];
    FILE *f;
    char *ptr = NULL;

    f = fopen(filename, "r");
    if (f == NULL) {
	D(("failed to read %s", filename));
	return ptr;
    }

    /* NB. offset should really be a field number since width of
       fields may change with OS release. */

    while (fgets(buffer, BLEN, f)) {
	if (!strncmp(token,buffer+field(buffer,field_number),strlen(token))) {

	    ptr = malloc(strlen(buffer));
	    if (ptr) {
		memcpy(ptr, buffer, strlen(buffer));
		ptr[strlen(buffer)-1] = '\0';
	    }
	    break;
	}
    }

    fclose(f);

    return ptr;
}

static void cleanup(pam_handle_t *pamh, void *data, int error_status)
{
    /* do nothing */
}

PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,
				   int argc, const char **argv)
{
#ifndef PROG
    int ctrl;
#endif /* !PROG */
    int retval = PAM_IGNORE;
    int pid;
    int timeout, remote_port, oslen, probe_fd;
    const char *ostype;
    char *value = malloc(BLEN);

    if (NULL == value) {
	D(("out of memory!"));
	return PAM_BUF_ERR;
    }

    ctrl = _pam_parse(&timeout, &remote_port, &ostype, &probe_fd,
#ifdef PROG
		      &pid,
#endif /* PROG */
		      argc, argv);
    oslen = strlen(ostype);

#ifndef PROG
    if (!(ctrl & NETID_REFRESH)) {
	int retvl;

	if (pam_get_data(pamh, "NETID_INFO", (void *)&retval) == PAM_SUCCESS) {
	    return (retvl-1);
	}
    }
#endif /* !PROG */

    /* loop through process tree - until we reach init. */
    while (pid > 1) {
	char *ptr;
	int fd = read_link("/proc/%d/fd/%d", pid, probe_fd);

	if (fd > 0) {

	    /* have we located a process with its standard input
	       connected to a INET socket? */
	    sprintf(value, "%d", fd);
	    ptr = find_value("/proc/net/tcp", 10, value);

	    if (ptr) {
		int sock, notok, from;
		const char *host;
		struct sockaddr_in conport;
		__u32 address;
		__u16 lport, rport;

		/* we have identified a network connection */
		if (ctrl & NETID_NONET) {
		    D(("user is accepting network traffic"));
		    retval = PAM_PERM_DENIED;
		}

		address = atoh(ptr+field(ptr,3));
		lport = (__u16) atoh(ptr+9+field(ptr,2));
		rport = (__u16) atoh(ptr+9+field(ptr,3));
		free(ptr);

		memset(&conport, 0, sizeof(conport));
		conport.sin_family = AF_INET;
		conport.sin_addr.s_addr = address;
		host = inet_ntoa(conport.sin_addr);
		conport.sin_port = htons(remote_port);
		sprintf(value, "%d , %d\n", rport, lport);

		D(("telnet %s %d\n%d, %d\n", host, remote_port, rport, lport));

		sock = socket(AF_INET, SOCK_STREAM, 6 /* TCP */);
		if (sock == -1) {
		    D(("failed to open a socket for identd"));
		    goto bad_result;
		}
		notok = connect(sock, (struct sockaddr *)&conport,
				sizeof(conport));
		if (notok) {
		    D(("failed to connect to remote socket"));
		    goto bad_result;
		}

		/* We are connected to the identd port */
		do {
		    notok = send(sock, value, strlen(value), 0);
		} while (notok == -1 && errno == EINTR);
		if (notok != strlen(value)) {
		    D(("poorly phrased query"));
		    goto bad_result;
		}

		/* wait for the reply */
		do {
		    struct timeval tval;
		    fd_set reader;

		    FD_ZERO(&reader);
		    FD_SET(sock, &reader);
		    tval.tv_sec  = timeout;
		    tval.tv_usec = 0;

		    notok = select(sock+1, &reader, NULL, NULL, &tval);
		    if (sock > 0 && FD_ISSET(sock, &reader)) {
			notok = recv(sock, value, BLEN-1, 0);
		    }
		} while (notok == -1 && errno == EINTR);

		if (notok <= 0) {
		    D(("bad reply"));
		    goto bad_result;
		}

		/* clean up */
		close(sock);
		sock = -1;

		value[notok] = '\0';

		/* expect ascii copy of remote port */
		from = field(value,1);
		if (rport != atoi(from+value)) {
		    D(("bad remote port comparison"));
		    goto bad_result;
		}
		for (; value[from] && isdigit(value[from]); ++from);
		from += field(from+value,1);
		if (value[from++] != ',') {
		    D(("bad syntax1 %s\n", value+from-1));
		    goto bad_result;
		}

		/* expect an echo of the local port */

		from += field(from+value,1);
		if (lport != atoi(from+value)) {
		    D(("bad local port comparison"));
		    goto bad_result;
		}
		for (; value[from] && isdigit(value[from]); ++from);
		from += field(from+value,1);
		if (value[from++] != ':') {
		    D(("bad syntax2"));
		    goto bad_result;
		}

		/* expect USERID : */

		from += field(from+value,1);
		if (strncmp(value+from,"USERID",6)) {
		    D(("got error: %s\n", value+from));
		    goto bad_result;
		}
		from += 6 + field(6+from+value,1);
		if (value[from++] != ':') {
		    D(("bad syntax3: %s", value+from-1));
		    goto bad_result;
		}

		/* make it one line - drop \n etc.. */
		for (notok=from; value[notok]; ++notok) {
		    switch (value[notok]) {
		    case '\r':
		    case '\n':
		    case '\t':
			value[notok] = ' ';
			break;
		    }
		}

		/* strip trailing spaces */
		for (notok=from+strlen(value+from);
		     --notok>from && isspace(value[notok]);
		     value[notok] = '\0');

		/* log result */
		from += field(from+value,1);
		if (strncmp(value+from,ostype,oslen)
		    || (from += oslen+field(oslen+from+value,1)),
		    value[from++] != ':' ) {
		    if (!(ctrl & NETID_NOLOG)) {
			_pam_log(LOG_NOTICE, "remote user [%s]@%s",
				 value+from-1, host);
		    }
		} else {
		    from += field(from+value,1);
		    if (!(ctrl & NETID_NOLOG)) {
			_pam_log(LOG_NOTICE, "remote %s user %s@%s",
				 ostype, value+from, host);
		    }
#ifndef PROG
		    if (ctrl & NETID_SETUSER) {
			pam_set_item(pamh, PAM_USER, value+from);
		    }
#endif /* !PROG */
		    if (ctrl & NETID_AUTHUSER) {
#ifdef PROG
			struct passwd *pwd = getpwuid(getuid());
			if (pwd && !strcmp(pwd->pw_name, value+from))
#else /* PROG */
			const char *pam_user=NULL;
			if (PAM_SUCCESS == pam_get_user(pamh, &pam_user,
							NULL)
			    && !strcmp(pam_user, value+from))
#endif /* PROG */
			{
			    /* if we agree with PAM's notion of the
			       user then we view this as a success */
			    retval = PAM_SUCCESS;
			} else {
			    /* since we are attempting to authenticate
			       we have to explicitly fail */
			    retval = PAM_AUTH_ERR;
			}
		    }
		}
		goto now_exit;

	    bad_result:
		if (sock != -1)
		    close(sock);
		if (!(ctrl & NETID_NOLOGERR)) {
		    _pam_log(LOG_NOTICE, "failed to identify remote user @%s",
			     host);
		}
		goto now_exit;

	    } else {
		D(("no net entry for %s\n", value));
		break;
	    }
	}
    
	sprintf(value, "/proc/%d/status", pid);
	ptr = find_value(value, 1, "PPid:");
	if (NULL == ptr) {
	    D(("no parent? (pid)\n", pid));
	    return PAM_ABORT;
	}
	pid = atoi(ptr+6);
	free(ptr);
    }

now_exit:

    if ((ctrl & NETID_ENFORCE) && retval == PAM_IGNORE) {
	retval = PAM_PERM_DENIED;
    }
    if (!(ctrl & NETID_REFRESH)) {
	int retvl;

	retvl = retval+1;
#ifndef PROG
	(void) pam_set_data(pamh, "NETID_INFO", (void *)retvl, cleanup);
#endif /* !PROG */
    }

    /* all done */
    return retval;
}

/* This function is blank */

PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags,
                          int argc, const char **argv)
{
    /* does nothing */
    return PAM_IGNORE;
}

#ifdef PROG
main(int argc, char **argv)
{
    exit(pam_sm_authenticate(NULL, 0, argc-1, 1+(const char **)argv));
}
#endif /* PROG */

/*
  Copyright (c) 1997 Andrew G. Morgan <morgan@linux.kernel.org>

Redistribution and use in source and binary forms of pam_netid, with
or without modification, are permitted provided that the following
conditions are met:

1. Redistributions of source code must retain any existing copyright
   notice, and this entire permission notice in its entirety,
   including the disclaimer of warranties.

2. Redistributions in binary form must reproduce all prior and current
   copyright notices, this list of conditions, and the following
   disclaimer in the documentation and/or other materials provided
   with the distribution.

3. The name of any author may not be used to endorse or promote
   products derived from this software without their specific prior
   written permission.

ALTERNATIVELY, this product may be distributed under the terms of the
GNU General Public License, in which case the provisions of the GNU
GPL are required INSTEAD OF the above restrictions.  (This clause is
necessary due to a potential conflict between the GNU GPL and the
restrictions contained in a BSD-style copyright.)

THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
-------------------------------------------------------------------------
 */
