/* authlocal.c 1.0.5 - auto-authenticate users connecting from localhost.
 *
 *	The procedure you want to call from your code is getUID,
 *	which returns a uid_t, or 0 if authentication failed.
 *	---->> Be sure to #define USE_AUTHLOCAL if you want it to work! <<----
 *
 *	Remember that uid's under 100 won't be recognized; this is because
 *	they're normally system daemon accounts, not real users.  If you
 *	need to change this behavior, change AUTHLOCAL_MIN_UID in authlocal.h
 *	before compiling.
 *
 * Copyright (C) 2002-2005 Jeremy Monin <jeremy@shadowlands.org>
 *	http://shadowlands.org/authlocal/
 * 1.0.6- 20051219.2350
 *    Note linux 2.6 support (no changes to source or algorithm).
 * 1.0.5- 20020704.1140
 *	Fixed fencepost error re matching lines on buffer boundary.
 * 1.0.4- 20020329.0140
 *	Added linux 2.4 support, many descriptive comments, efficiency
 *	improvements, and C-comments, not C++-comments (no //)
 * original ver. was 19990907.1515
 */

#include <unistd.h>  /* for ssize_t, uid_t */
#include <stdio.h>
#include <stdlib.h>  /* for atol */
#include <sys/types.h>  /* for open */
#include <sys/stat.h>   /* for open */
#include <fcntl.h>      /* for open */
#include <string.h>  /* for memmove */
#include <sys/socket.h>  /* this is   */
#include <netinet/in.h>  /* all for   */
#include <arpa/inet.h>   /* inet_addr */

#include "authlocal.h"

/* -- for debug -- 

#define DEBUG_AUTHLOCAL 1
#define USE_AUTHLOCAL 1
char* tcpstat_fname_str = "/proc/net/tcp";
#define TCPSTAT_FNAME tcpstat_fname_str

 -- /for debug -- */


/*
 * How it Works:
 *
 * When connecting to a TCP server from localhost, the server knows
 * the arbitrary port# you're connecting from... under linux, the file
 * /proc/net/tcp contains data about this socket, including its owner.
 * This can be used to automatically, securely authenticate.
 * See an example at the bottom of this file.
 *
 * It does lead to the problem of not _wanting_ to log in as yourself,
 * but this is a separate issue.  Also, be sure root doesn't log in this way.
 * We also should check to be sure the logging-in user has a valid shell,
 * but we don't right now.
 *
 * Tested and successful under linux 2.0.x, 2.2.x, 2.4.x, 2.6.x
 *	through 2.6.11.
 *
 * Be sure to define USE_AUTHLOCAL if you want to use this;
 * if this isn't defined, the entire getUID function will ALWAYS
 * return 0 (failure).  This means you don't need to change any of
 * your other code, assuming you have a fallback method for authenticating
 * people, which you'll need anyway if you want them to be able to
 * get in from other hosts.
 */

/*
    authlocal.c version 1.0.x - Automatic authentication of connections
    from localhost.
    Copyright (C) 2000-2005 Jeremy D. Monin (jeremy@shadowlands.org).

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/



#ifdef USE_AUTHLOCAL

	/* yes, most of the file's in this ifdef. */
	/* Only thing that isn't is, if USE_AUTHLOCAL
	 * isn't defined, the function that gets called
	 * from outside this module always returns 0 (failure),
	 * meaning you should fall back to standard username-and-pw-prompt
	 * authentication.
	 */


/* format of /proc/net/tcp lines: (as of linux v2.0-2.6 as of this writing)
 * We want to get the UID.
 *    sl    addy       addy   st dunno:d   d:dunno uh uid timeout inode ...
 * \n  n: local:port rem:port nn nnnn:nnn nn:nnnn nnn uid  n n ...
 *        ^^^^^^^^    ^^^^^  in hex; each is 8:4 chars 
 *  the ... after inode were added after 2.2.x.
 */

/* PATSIZE is used in allocating buffer space AND in moving the char ptr;
 * it MUST be the correct length (incl trailing \0).
 */
#define PATSIZE    (8+1+4+1+8+1+4+1)  /* "0100007f:xxxx 0100007f:xxxx\0" */
#define LINEBUFSIZ 4096               /* must be much bigger than line len */
#ifndef TCPSTAT_FNAME
#define TCPSTAT_FNAME "/proc/net/tcp"
#endif


/* check to see if we need to read more chars into the buffer;
 * if so, do that and adjust the end-of-buffer-data pointer.
 * Either way, return it.  Return NULL for EOF.
 * On entry, *c points to the current buffer pos (the part we must keep).
 * (We pass **c so we can change the current buffer pos pointer in the calling
 *	procedure.)
 * *c must be past the start of the buffer. (*c != buf)  Otherwise how can
 * we have any room to read new data into?
 */
static char *read_chunk (char *buf, char **c, char *bufend, int inputfd)
{
  ssize_t nread;
  int inbufloc = *c - buf;  /* how far into buffer are we? */

  /* Must read some into the buffer, but retain partial lines (retain what's
   * between *c and *bufend)...  First, move current buffer contents up so we
   * have room for more, discarding (overwriting) what's currently before *c
   * in the buffer.
   */
  if (inbufloc > 0)
  {
	  memmove (buf, *c, LINEBUFSIZ - inbufloc);
	  *c = buf + (LINEBUFSIZ - inbufloc); 
	  /* *c now points right after the end of the current content;
	   * but in some cases this will be null-terminated, so we need to
	   * not retain any \0's in the current content.
	   */
	  --(*c);
	  while (( *c > buf ) && (**c == '\0'))
		  --(*c);
	  if (**c != '\0') ++(*c);
	  /* *c now points to where new stuff should be added; null-term it. 
	   * we'll adjust *c again soon, but this is where we need it for now.
	   */
	  **c = '\0';
  }    

  /* so add the new stuff, try to fill to end of buffer */
  nread = read (inputfd, *c, LINEBUFSIZ - (*c - buf) - 1);
  if (nread == -1) {
#ifdef DEBUG_AUTHLOCAL
    perror ("read");
#endif
    return NULL;
  }
  if (nread == 0)    /* EOF */
    return NULL;

  /* adjust c to start of buf, adjust end of buf ptr and return it */
  nread += (*c - buf);
  *(buf + nread) = '\0';
  *c = buf;

  return (buf + nread);
}


/** Begin reading from a file; return the end-of-buffer-data pointer,
 *  a pointer (inside buf) to the \0 right after the end of where we read,
 *  or return NULL if we can't read.
 */
static char *buf_begin_get(char *buf, int readfromfd)
{
  ssize_t nread = read (readfromfd, buf, LINEBUFSIZ-1);
  if (nread == -1) {
    return NULL;
  }

  *(buf + nread) = '\0';
  return (buf + nread);
}



/* returns 0 if something goes wrong and it can't be found.
 * (UID 0 really shouldn't be logging in from a network anyway.)
 */
static uid_t getUID_impl (unsigned int /* type? */ localhost_ip,
              int /* type? */ userportno, int daemonportno,
              int inputfd)
{
  char wantthis_in_hex[PATSIZE];
  char buf[LINEBUFSIZ];
  char *b;
  char *bufend;
  char *c;

  sprintf (wantthis_in_hex, "%08X:%04X %08X:%04X",
	   localhost_ip, (int) userportno, localhost_ip, (int) daemonportno);


  /* search for a line containing this */
  bufend = buf_begin_get (buf, inputfd);
  if (bufend == NULL) {
    return (uid_t) 0;  /* couldn't read */
  }

  /* we have c point at our current place in the buffer we search;
   * as we read more, read_chunk will adjust c as the buffer moves.
   */
  for ( c = buf;
	(bufend != NULL) && (*c != '\0') && (c < bufend);
	bufend = read_chunk (buf, &c, bufend, inputfd))
  {
    /* remember our place, in case c is about to become NULL */
    b = c;
    /* try to get c to point to the string we seek  */
    c = strstr (c, wantthis_in_hex);

    if (c != NULL)
    {
      /*  Found a match to our pattern.
       *  Now get uid and return it; we must skip ahead from where we
       *  found our src/dest port pair; to go from the end of our match
       *  to uid, we find and skip 5 blank areas (incl the one immediately
       *  after end of match).
       *  If we run out of string (find \0) before then, we have a match on
       *  a partial line and need to read more of the file into the buffer.
       */
      int bkfld;

      /* point c at end of pattern */
      c += (PATSIZE - 1);

      /* we're now at white space.  We must skip 5 whitespace areas, and
       * the 4 fields (non-whitespace) between them.  Right after the 5th
       * whitespace area is the uid field.  We skip over that too, because
       * we want to null-terminate that and then go back to its start.
       */
      for (bkfld = 1; bkfld <= 5; ++bkfld)
      {
	/* inch c forward through whitespace */
	while (*c <= ' ')
	{
		if (*c == '\0')
		{
			/* we're on a partial line, must read more file into
			 * the buffer and then retry the search.
			 */
			c = NULL;
			break;
		}
		++c;
	}
	if (c == NULL) break;

	/* now inch c forward through non-whitespace */
	while (*c > ' ')
		++c;
      }

      if (c != NULL)
      {
	/* we're now right after end of UID; null-terminate it and move back */
        *c = '\0';  --c;
	
        /* this is the field data; inch backward to next whitespace */
        while (*c > ' ')
	      --c;
	    
        /* now nudge forward and read it!  have a nice day,
         * we're done.
         */
        ++c;
        /* read it */
        return (uid_t) atol(c);   /*  <-------- return the uid ------  */

      }  /* end if c-not-null (partial line after match) */
    }  /* end if c-not-null (no match at all) */

    /* not found, grab more (to search through) from file into buffer, but
     * we must retain the entire last line or two of the current buffer, in
     * case we have just part of that line right now, and it's the right line
     * but we don't know it because we don't have the entire line.
     */
    c = bufend - 1;
    /* find start of final line: */
    while ((*c != '\n') && (c != buf))
	--c;
    if ((c == buf) && (*c != '\n'))
    {
	    /* found no \n in the whole buffer; a very long line.
	     * the line must go away to make room. (not likely to happen)
	     */
	    c = bufend;
    } else {
	    char *lastlinec = c;   /* remember start of final line */
	    /* find start of next-to-final line: */
	    while ((*c != '\n') && (c != buf))
		    --c;
	    if ((c == buf) && (*c != '\n'))
		    c = lastlinec;  /* can't keep all of the 2 lines: keep 1 */
    }
    
  }  /* end while-not-eof loop */

  return (uid_t) 0;  /*  <-------- couldn't find it. ----------  */
}




#endif	/* the big IFDEF */



/* Finally, here's what we need!
 * Returns 0 if something goes wrong and it can't be found.
 * (UID 0 really shouldn't be logging in from a network anyway.)
 */
uid_t getUID (unsigned int localhost_ip,
              int userport, int daemonport)
{

#ifndef USE_AUTHLOCAL
  return (uid_t) 0;
#else
  uid_t retval;

  int fd = open(TCPSTAT_FNAME, O_RDONLY);
  if (fd == -1) {
    return (uid_t) 0;
  }
  retval = getUID_impl (localhost_ip, userport, daemonport, fd);
  close (fd);

  if (retval > AUTHLOCAL_MIN_UID)
    return retval;
  else
    return 0;

#endif	/* ifdef (USE_AUTHLOCAL) */
}


/* code for usage demonstration purposes.  Note that it's commented out.

		static struct in_addr localhost_ip;
		localhost_ip.s_addr = htonl(0x7f000001);
	...
		struct 	sockaddr_in user_address;
		int user_fd = accept
		  ( server_fd, (sockaddr *)&user_address, &len );
	...
		uid_t authenticated = 0;
		if (user_address.sin_addr.s_addr == localhost_ip.s_addr ) {

		Auto-Authenticate user if logging in from localhost!

		  authenticated = getUID
		    ( localhost_ip.s_addr, ntohs(user_address.sin_port),
		      server_portnum );
		}


int main(int argc, char **argv)
{
  int pnum_cli = 2048;
  int pnum_srv = 5000;
  int retuid;

  if (argc > 1)
  {
    if (strncasecmp(argv[1], "0x", 2))
      pnum_cli = (int) strtol(argv[1], (char **)NULL, 10);
    else
      pnum_cli = (int) strtol(argv[1], (char **)NULL, 16);
  }
  if (argc > 2)
  {
    if (strncasecmp(argv[2], "0x", 2))
      pnum_srv = (int) strtol(argv[2], (char **)NULL, 10);
    else
      pnum_srv = (int) strtol(argv[2], (char **)NULL, 16);
  }
#ifdef DEBUG_AUTHLOCAL
  if (argc > 3)
	  tcpstat_fname_str = argv[3];
  printf ("file for /proc/net/tcp is %s\n", tcpstat_fname_str);
#endif

  printf ("lookin' for a connection from port %d (0x%04X) to %d (0x%04X)\n",
	pnum_cli, pnum_cli, pnum_srv, pnum_srv);
  retuid = (int) getUID ( inet_addr ("127.0.0.1"), pnum_cli, pnum_srv);
  printf ("returned %d\n", retuid);

  return (retuid ? 0 : 1);
}


 authlocal.c ENDS. */
