/* Submit : A Course Project Submission Program
 *
 * Copyright (C) 1998 Alexander V. Konstantinou (akonstan@acm.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., 59 Temple Place - Suite 330, Boston,
 * MA 02111-1307, USA.
 *
 * $Id: SocketIdentification.java,v 1.2 2001/11/08 22:57:55 akonstan Exp $
 */

package org.acm.akonstan.net;

import java.io.*;
import java.net.*;
import java.util.Date;
import java.text.DateFormat;
 
/**
 * Implements the Identification Protocol proposed in the IETF RFC 1413.
 * <p>
 * The Identification Protocol (a.k.a., ``ident'', a.k.a., ``the Ident
 * Protocol'') provides a means to determine the identity of a user of a
 * particular TCP connection.  Given a TCP port number pair, it returns
 * a character string which identifies the owner of that connection on
 * the server's system. 
 * <p>
 * Instances of <code>SocketIdentification</code> are created using the
 * static factory method identify()
 * <p>
 * <strong>Note</strong> - only handles US-ASCII encoded replies,
 * throws ParseError exception for other character encodings.
 *
 * @see <a href="http://www.ietf.org/rfc/rfc1413.txt">IETF RFC 1413</a>
 *
 * @version $Revision: 1.2 $ ; $Date: 2001/11/08 22:57:55 $
 * @author Alexander V. Konstantinou (akonstan@acm.org)
 */    
public class SocketIdentification {
  /** Well known TCP port of the Ident Protocol */
  public static final int IDENT_PORT = 113;

  /** Default identification response timeout (to handle firewalls) */
  public static final int IDENT_CLIENT_TIMEOUT_MS = 30000;

  private int portOnServer = -1;
  private int portOnClient = -1; 
  private String charSet = null;
  private String userID = null;
  private String opSys = null;
  private String errorType;  

  /**
   * Instances created using the static factory method <code>identify()</code>
   */
  protected SocketIdentification() {
  }

  /**
   * Returns an instance of SocketIdentification containing the
   * userID and opSys values returned, or an errorType.
   *
   * @param sock - the socket whose user needs to be identified
   *
   * @exception java.io.IOException indicating a communication error
   * @exception java.io.ParseException with an appropriate message
   *   when unable to parse the server's response
   */
  public static SocketIdentification identify(Socket sock) 
    throws java.io.IOException, java.text.ParseException {
    return(identify(sock, IDENT_CLIENT_TIMEOUT_MS));
  }

  /**
   * Returns an instance of SocketIdentification containing the
   * userID and opSys values returned, or an errorType.
   *
   * @param sock - the socket whose user needs to be identified
   * @param timeoutMillis - the identification request timeout
   *
   * @exception java.io.IOException indicating a communication error
   * @exception java.io.ParseException with an appropriate message
   *   when unable to parse the server's response
   */
  public static SocketIdentification identify(Socket sock, int timeoutMillis) 
    throws java.io.IOException, java.text.ParseException {

    if (sock == null)
      throw new NullPointerException("null socket argument");

    if (timeoutMillis < 0)
      throw new IllegalArgumentException("negative response timeout argument");

    SocketIdentification ident = new SocketIdentification();

    ident.portOnServer = sock.getPort();
    ident.portOnClient = sock.getLocalPort();

    Socket idsocket = null;
    String response = null;

    try {
      idsocket = new Socket(sock.getInetAddress(), IDENT_PORT);
      idsocket.setSoTimeout(timeoutMillis);

      //
      // create QUERY string
      //
      // <port-on-server> , <port-on-client>
      OutputStream os = idsocket.getOutputStream();
      String message = ident.portOnServer + ", " + ident.portOnClient + "\n";
      os.write(message.getBytes("8859_1"));

      //
      // receive RESPONSE
      //
      InputStream is = idsocket.getInputStream();
      byte[] replyOctet = new byte[4096];
      int n = is.read(replyOctet);
      response = new String(replyOctet, "8859_1");
    } catch (ConnectException e) {
      throw new ConnectException(e.getMessage() + 
				 ": the ident service is not available");
    } finally {
      if (idsocket != null) idsocket.close();
    } 
    
    /* parse RESPONSE :
     *
     * <reply> ::= <reply-text> <EOL>
     * <EOL> ::= "015 012"  ; CR-LF End of Line Indicator
     * <error-reply> ::= <port-pair> ":" "ERROR" ":" <error-type>
     * <ident-reply> ::= <port-pair> ":" "USERID" ":" <opsys-field> 
     *                   ":" <user-id>
     * <error-type> ::= "INVALID-PORT" | "NO-USER" | "UNKNOWN-ERROR"
     *                   | "HIDDEN-USER" |  <error-token>
     * <opsys-field> ::= <opsys> [ "," <charset>]
     * <opsys> ::= "OTHER" | "UNIX" | <token> ...etc.
     * <charset> ::= "US-ASCII" | ...etc.
     * <user-id> ::= <octet-string>
     * <token> ::= 1*64<token-characters> ; 1-64 characters
     * <error-token> ::= "X"1*63<token-characters>
     */

    int current, next;

    // port-pair is ignored since we already know it (I guess we could
    // verify it if we were paranoid)
    current = response.indexOf(':', 0);
    if (current == -1) {
      throw new java.text.ParseException
	("Error parsing Ident response (response does not contain ':'", 0);
    }
    current++;

    // extract response type
    next = response.indexOf(':', current);
    if (next == -1) {
      throw new java.text.ParseException
	("Error parsing Ident response (response type not followed by ':'",
	 current);
    }
    
    String responseType = response.substring(current, next).trim();

    current = next+1;

    // parse response type
    if (responseType.equalsIgnoreCase("ERROR")) {
      // parse error response
      next = response.indexOf('\015', current);
      if (next == -1) {
	// don't complain about non-compliant servers !
	ident.errorType = response.substring(current).trim();
      } else {
	ident.errorType = response.substring(current, next).trim();
      }
    } else if (responseType.equalsIgnoreCase("USERID")) {
      // parse opsys field in response
      next = response.indexOf(':', current);
      if (next == -1) {
	throw new java.text.ParseException
	  ("Error parsing Ident response (userid response not followed " + 
	   "by ':'", current);
      }
      ident.opSys = response.substring(current, next).trim();
      ident.charSet = "US-ASCII"; // default
      current = next + 1;

      // check for optional character set specification
      int commaPos = ident.opSys.lastIndexOf(',');
      if (commaPos != -1) {
	//
	ident.charSet = ident.opSys.substring(commaPos + 1).trim();
	ident.opSys = ident.opSys.substring(0, commaPos).trim();
	
	if (ident.charSet.equals("US-ASCII")) {
	  // no need to modify anything, already parsed as ISO-8859-1
	} else {
	  // at this point, we would need to convert the raw response
	  // as the new character-set.  Not implemented - throw exception
	  throw new java.text.ParseException
	    ("Unsupported used ID character encoding " + ident.charSet, 
	     current);
	}
      }
      
      // parse user-id field
      next = response.indexOf('\015', current);
      if (next == -1) {
	// don't complain about non-compliant servers !
	ident.userID = response.substring(current).trim();
      } else {
	ident.userID = response.substring(current, next).trim();
      }
    } else {
      throw new java.text.ParseException("Unknown Ident response type " +
					 responseType, current);
    }
    
    return ident;
  } // identify


  /**
   * @return the originating port at the server running the Ident daemon
   */
  public int getPortOnServer() {
    return portOnServer;
  }

  /**
   * @return the destination port number at the local host
   */
  public int getPortOnClient() {
    return portOnClient;
  }

  /**
   * @return the user ID as reported by Ident, or null if the identification
   *         process was aborted.
   */
  public String getUserID() {
    return userID;
  }

  /**
   * @return the operating system name as reported by Ident, or null if 
   *         the identification process was aborted.
   */
  public String getOpSys() {
    return opSys;
  }

  /**
   * @return a string description of the error type, or null if no error
   *         has occured.
   */
  public String getErrorType() {
    return errorType;
  }

  /**
   * Used for debuging and testing the identification information passed
   * by a host.
   */
  public static void main(String args[]) {
    ServerSocket sock = null;
    int serverPort = 6789;

    if (args.length == 1) {
      try {
	serverPort = Integer.parseInt(args[0]);
      } catch (NumberFormatException e) {
	System.err.println("Invalid socket number " + args[0]);
	System.err.println("Usage: SocketIdentification { <port_number> }");
	System.exit(1);
      }
    }

    try {
      sock = new ServerSocket(serverPort); // test port
      System.out.println("SocketIdentification tester listening on port " +
                         sock.getLocalPort() + "\n");
      System.out.println("You may test your identification by telneting " +
			 "from this, or another host\nto this server port.\n");
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
    
    while(true) {
      Socket sockIn = null;

      try {
	sockIn = sock.accept();
	
	Date now = new Date();
	
	System.out.println("Connect from " +
			   sockIn.getInetAddress() + " to port " +
			   sockIn.getLocalPort() + " on " +
			   DateFormat.getDateInstance().format(now) + " " +
			   DateFormat.getTimeInstance().format(now));
      
	try {
	  SocketIdentification ident = SocketIdentification.identify(sockIn);
	  System.out.println("User ID=" + ident.getUserID());
	  System.out.println("Operating System=" + ident.getOpSys());
	} catch(Throwable e) {
	  System.err.println(e.getClass().getName() + ": " + e.getMessage());
	}
	
      } catch (Throwable e) {
	System.err.println(e.getClass().getName() + ": " +
			   e.getMessage());
      } finally {
	if (sockIn != null) {
	  try { sockIn.close(); } catch (Throwable e2) { }
	}
      }
    }    
  } // main                        
} // class SocketIdentification

