package jcsi;

import java.util.Properties;
import java.io.*;
import java.net.*;

import com.dstc.security.kerberos.Kerberos;
import com.dstc.security.kerberos.KerberosContext;
import com.dstc.security.kerberos.gssapi.*;

public class GSSClient
{
  private InputStream in;
  private OutputStream out;

  /**
   * Constructs a GSSClient with a given input and out stream
   */
  public GSSClient(InputStream inStream, OutputStream outStream)
  {
    this.in = inStream;
    this.out = outStream;
  }

  public static void main(String args[])
  {
    int port = 4445;
    boolean deleg = false;
    String host = null;
    String service = null;
    String msg = null;

    if ((args.length == 5) && args[0].equals("-port"))
    {
      port = java.lang.Integer.parseInt(args[1]);
      host = args[2];
      service = args[3];
      msg = args[4];
    }
    else if ((args.length == 6)
             && args[0].equals("-port")
             && args[2].equals("-d"))
    {
      port = java.lang.Integer.parseInt(args[1]);
      deleg = true;
      host = args[3];
      service = args[4];
      msg = args[5];
    }
    else if (args.length == 3)
    {
      host = args[0];
      service = args[1];
      msg = args[2];
    }
    else if ((args.length == 4) && args[0].equals("-d"))
    {
      deleg = true;
      host = args[1];
      service = args[2];
      msg = args[3];
    }
    else
    {
      System.out.println(
        "Usage: jgss-client [-port port] [-d] host service msg");
      System.exit(0);
    }

    msg = msg + '\0';

    try
    {
      Socket socket
        = new Socket(InetAddress.getByName(host), port);
      BufferedOutputStream bos
        = new BufferedOutputStream(socket.getOutputStream());
      DataInputStream dis
        = new DataInputStream(new BufferedInputStream(socket.getInputStream()));

      GSSClient client = new GSSClient(dis, bos);

      // start by creating the name for a service entity
      GSSName targetName = new GSSName(service,
                                       GSSName.NT_HOSTBASED_SERVICE);

      // create a context using default credentials for the
      // default mechanism
      GSSContext aCtxt = new GSSContext(targetName, null, null,
                                        GSSContext.INDEFINITE);

      // set desired context options
      if (deleg)
        aCtxt.requestCredDeleg(true);

      aCtxt.requestMutualAuth(true);
      aCtxt.requestSequenceDet(true);
      aCtxt.requestConf(true);

      // establish a context between peers - using byte arrays
      byte[] inTok = new byte[0];

      do
      {
        byte[] outTok = aCtxt.init(inTok, 0, inTok.length);

        // send the token if present
        if (outTok != null)
        {
          System.out.println("Sending init_sec_context token...");
          client.sendToken(outTok);
        }

        // check if we should expect more tokens
        if (aCtxt.isEstablished())
          break;

        // another token expected from peer
        System.out.println("continue needed...");
        inTok = client.readToken();

      } while (true);
      
      // display context flags
      if (aCtxt.getCredDelegState())
        System.out.println("context flag: DELEG_FLAG");
      if (aCtxt.getMutualAuthState())
        System.out.println("context flag: MUTUAL_FLAG");
      if (aCtxt.getReplayDetState())
        System.out.println("context flag: REPLAY_FLAG");
      if (aCtxt.getSequenceDetState())
        System.out.println("context flag: SEQUENCE_FLAG");
      if (aCtxt.getConfState())
        System.out.println("context flag: CONF_FLAG");
      if (aCtxt.getIntegState())
        System.out.println("context flag: INTEG_FLAG");

      // display context information
      System.out.println("\"" + aCtxt.getSrcName().toString()
                         + "\" to \"" + aCtxt.getTargName().toString()
                         + "\", lifetime " + aCtxt.getLifetime());

      System.out.println("Name type of source name is "
        + aCtxt.getSrcName().getStringNameType().toRFC2078String());

      System.out.println("Mechanism " + aCtxt.getMech().toRFC2078String()
                         + " supports");

      Oid[] nameTypes = GSSManager.getNamesForMech(aCtxt.getMech());
      for (int i = 0; i < nameTypes.length; i++)
        System.out.println(" " + nameTypes[i].toRFC2078String());

      // seal the message
      byte[] appMsg = msg.getBytes();
      MessageProp mProp = new MessageProp();
      mProp.setPrivacy(true);
      byte[] tok = aCtxt.wrap(appMsg, 0, appMsg.length, mProp);

      if (!mProp.getPrivacy())
        System.out.println("Warning! Message not encrypted.");

      // send to server
      client.sendToken(tok);

      // read signature
      tok = client.readToken();

      // verify signature
      mProp = new MessageProp();
      aCtxt.verifyMIC(tok, 0, tok.length, appMsg, 0, appMsg.length, mProp);
      System.out.println("Signature verified.");

      if (mProp.isUnseqToken())
        System.out.println("Out-of-sequence token received.");
      if (mProp.isGapToken())
        System.out.println("Gap token received.");

      // delete context
      aCtxt.dispose();
    }
    catch (GSSException e)
    {
      System.out.println("GSS-API error: " + e.getMessage());
    }
    catch (Exception e)
    {
      System.out.println(e.getMessage());
    }
  }

  /**
   * Writes a supplied Token in the form of a byte array to the output stream
   */
  public void sendToken(byte[] data) throws IOException
  {
    int len = data.length;

    byte[] lengthBytes = new byte[4];
    lengthBytes[0] = (byte)((len >>> 24) & 0xff);
    lengthBytes[1] = (byte)((len >>> 16) & 0xff);
    lengthBytes[2] = (byte)((len >>>  8) & 0xff);
    lengthBytes[3] = (byte)(len & 0xff);

    this.out.write(lengthBytes);
    this.out.write(data);
    this.out.flush();
  }

  /**
   * Reads a Token from the input stream and returns it as a byte array
   */
  public byte[] readToken() throws IOException
  {
    byte[] lengthBytes = new byte[4];
    this.in.read(lengthBytes);
    int length =  ((int)(lengthBytes[0] & 0xff) << 24) +
                  ((int)(lengthBytes[1] & 0xff) << 16) +
                  ((int)(lengthBytes[2] & 0xff) << 8) +
                  ((int)(lengthBytes[3] & 0xff) << 0);

    byte[] retval = new byte[length];
    this.in.read(retval);
    return retval;
  }

  public static void print (byte[] bytes)
  {
      for (int i = 0; i< bytes.length; i++)
        System.out.print (java.lang.Integer.toHexString(bytes[i] & 0xff) + " ");

      System.out.println (" ");
  }
}
