/* jcifs smb client library in Java
 * Copyright (C) 2002  "Michael B. Allen" <jcifs at samba dot org>
 *                 "Eric Glass" <jcifs at samba dot org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package jcifs.ntlmssp;

import java.io.IOException;

import java.net.UnknownHostException;

import jcifs.Config;

import jcifs.netbios.NbtAddress;

/**
 * Represents an NTLMSSP Type-2 message.
 */
public class Type2Message extends NtlmMessage {

    private static final int DEFAULT_FLAGS;

    private static final String DEFAULT_DOMAIN;

    private static final byte[] DEFAULT_TARGET_INFORMATION;

    private byte[] challenge;

    private String target;

    private byte[] context;

    private byte[] targetInformation;

    static {
        DEFAULT_FLAGS = NTLMSSP_NEGOTIATE_NTLM |
                (Config.getBoolean("jcifs.smb.client.useUnicode", true) ?
                        NTLMSSP_NEGOTIATE_UNICODE : NTLMSSP_NEGOTIATE_OEM);
        DEFAULT_DOMAIN = Config.getProperty("jcifs.smb.client.domain", null);
        byte[] domain = new byte[0];
        if (DEFAULT_DOMAIN != null) {
            try {
                domain = DEFAULT_DOMAIN.getBytes(UNI_ENCODING);
            } catch (IOException ex) { }
        }
        int domainLength = domain.length;
        byte[] server = new byte[0];
        try {
            String host = NbtAddress.getLocalHost().getHostName();
            if (host != null) {
                try {
                    server = host.getBytes(UNI_ENCODING);
                } catch (IOException ex) { }
            }
        } catch (UnknownHostException ex) { }
        int serverLength = server.length;
        byte[] targetInfo = new byte[(domainLength > 0 ? domainLength + 4 : 0) +
                (serverLength > 0 ? serverLength + 4 : 0) + 4];
        int offset = 0;
        if (domainLength > 0) {
            writeUShort(targetInfo, offset, 2);
            offset += 2;
            writeUShort(targetInfo, offset, domainLength);
            offset += 2;
            System.arraycopy(domain, 0, targetInfo, offset, domainLength);
            offset += domainLength;
        }
        if (serverLength > 0) {
            writeUShort(targetInfo, offset, 1);
            offset += 2;
            writeUShort(targetInfo, offset, serverLength);
            offset += 2;
            System.arraycopy(server, 0, targetInfo, offset, serverLength);
        }
        DEFAULT_TARGET_INFORMATION = targetInfo;
    }

    /**
     * Creates a Type-2 message using default values from the current
     * environment.
     */
    public Type2Message() {
        this(getDefaultFlags(), null, null);
    }

    /**
     * Creates a Type-2 message in response to the given Type-1 message
     * using default values from the current environment.
     *
     * @param type1 The Type-1 message which this represents a response to.
     */
    public Type2Message(Type1Message type1) {
        this(type1, null, null);
    }

    /**
     * Creates a Type-2 message in response to the given Type-1 message.
     *
     * @param type1 The Type-1 message which this represents a response to.
     * @param challenge The challenge from the domain controller/server.
     * @param target The authentication target.
     */
    public Type2Message(Type1Message type1, byte[] challenge, String target) {
        this(getDefaultFlags(type1), challenge, (type1 != null &&
                target == null && type1.getFlag(NTLMSSP_REQUEST_TARGET)) ?
                        getDefaultDomain() : target);
    }

    /**
     * Creates a Type-2 message with the specified parameters.
     *
     * @param flags The flags to apply to this message.
     * @param challenge The challenge from the domain controller/server.
     * @param target The authentication target.
     */
    public Type2Message(int flags, byte[] challenge, String target) {
        setFlags(flags);
        setChallenge(challenge);
        setTarget(target);
        if (target != null) setTargetInformation(getDefaultTargetInformation());
    }

    /**
     * Creates a Type-2 message using the given raw Type-2 material.
     *
     * @param material The raw Type-2 material used to construct this message.
     * @throws IOException If an error occurs while parsing the material.
     */
    public Type2Message(byte[] material) throws IOException {
        parse(material);
    }

    /**
     * Returns the challenge for this message.
     *
     * @return A <code>byte[]</code> containing the challenge.
     */
    public byte[] getChallenge() {
        return challenge;
    }

    /**
     * Sets the challenge for this message.
     *
     * @param challenge The challenge from the domain controller/server.
     */
    public void setChallenge(byte[] challenge) {
        this.challenge = challenge;
    }

    /**
     * Returns the authentication target.
     *
     * @return A <code>String</code> containing the authentication target.
     */
    public String getTarget() {
        return target;
    }

    /**
     * Sets the authentication target.
     *
     * @param target The authentication target.
     */
    public void setTarget(String target) {
        this.target = target;
    }

    /**
     * Returns the target information block.
     *
     * @return A <code>byte[]</code> containing the target information block.
     * The target information block is used by the client to create an
     * NTLMv2 response.
     */ 
    public byte[] getTargetInformation() {
        return targetInformation;
    }

    /**
     * Sets the target information block.
     * The target information block is used by the client to create
     * an NTLMv2 response.
     * 
     * @param targetInformation The target information block.
     */
    public void setTargetInformation(byte[] targetInformation) {
        this.targetInformation = targetInformation;
    }

    /**
     * Returns the local security context.
     *
     * @return A <code>byte[]</code> containing the local security
     * context.  This is used by the client to negotiate local
     * authentication.
     */
    public byte[] getContext() {
        return context;
    }

    /**
     * Sets the local security context.  This is used by the client
     * to negotiate local authentication.
     *
     * @param context The local security context.
     */
    public void setContext(byte[] context) {
        this.context = context;
    }

    public byte[] toByteArray() {
        try {
            String targetName = getTarget();
            byte[] challenge = getChallenge();
            byte[] context = getContext();
            byte[] targetInformation = getTargetInformation();
            int flags = getFlags();
            byte[] target = new byte[0];
            if ((flags & NTLMSSP_REQUEST_TARGET) != 0) {
                if (targetName != null && targetName.length() != 0) {
                    target = (flags & NTLMSSP_NEGOTIATE_UNICODE) != 0 ?
                            targetName.getBytes(UNI_ENCODING) :
                            targetName.toUpperCase().getBytes(getOEMEncoding());
                } else {
                    flags &= (0xffffffff ^ NTLMSSP_REQUEST_TARGET);
                }
            }
            if (targetInformation != null) {
                flags |= NTLMSSP_NEGOTIATE_TARGET_INFO;
                // empty context is needed for padding when t.i. is supplied.
                if (context == null) context = new byte[8];
            }
            int data = 32;
            if (context != null) data += 8;
            if (targetInformation != null) data += 8;
            byte[] type2 = new byte[data + target.length +
                    (targetInformation != null ? targetInformation.length : 0)];
            System.arraycopy(NTLMSSP_SIGNATURE, 0, type2, 0, 8);
            writeULong(type2, 8, 2);
            writeSecurityBuffer(type2, 12, data, target);
            writeULong(type2, 20, flags);
            System.arraycopy(challenge != null ? challenge : new byte[8], 0,
                    type2, 24, 8);
            if (context != null) System.arraycopy(context, 0, type2, 32, 8);
            if (targetInformation != null) {
                writeSecurityBuffer(type2, 40, data + target.length,
                        targetInformation);
            }
            return type2;
        } catch (IOException ex) {
            throw new IllegalStateException(ex.getMessage());
        }
    }

    public String toString() {
        String target = getTarget();
        byte[] challenge = getChallenge();
        byte[] context = getContext();
        byte[] targetInformation = getTargetInformation();

        return "Type2Message[target=" + target +
            ",challenge=" + (challenge == null ? "null" : "<" + challenge.length + " bytes>") +
            ",context=" + (context == null ? "null" : "<" + context.length + " bytes>") +
            ",targetInformation=" + (targetInformation == null ? "null" : "<" + targetInformation.length + " bytes>") +
            ",flags=0x" + jcifs.util.Hexdump.toHexString(getFlags(), 8) + "]";
    }

    /**
     * Returns the default flags for a generic Type-2 message in the
     * current environment.
     *
     * @return An <code>int</code> containing the default flags.
     */
    public static int getDefaultFlags() {
        return DEFAULT_FLAGS;
    }

    /**
     * Returns the default flags for a Type-2 message created in response
     * to the given Type-1 message in the current environment.
     *
     * @return An <code>int</code> containing the default flags.
     */
    public static int getDefaultFlags(Type1Message type1) {
        if (type1 == null) return DEFAULT_FLAGS;
        int flags = NTLMSSP_NEGOTIATE_NTLM;
        int type1Flags = type1.getFlags();
        flags |= ((type1Flags & NTLMSSP_NEGOTIATE_UNICODE) != 0) ?
                NTLMSSP_NEGOTIATE_UNICODE : NTLMSSP_NEGOTIATE_OEM;
        if ((type1Flags & NTLMSSP_REQUEST_TARGET) != 0) {
            String domain = getDefaultDomain();
            if (domain != null) {
                flags |= NTLMSSP_REQUEST_TARGET | NTLMSSP_TARGET_TYPE_DOMAIN;
            }
        }
        return flags;
    }

    /**
     * Returns the default domain from the current environment.
     *
     * @return A <code>String</code> containing the domain.
     */
    public static String getDefaultDomain() {
        return DEFAULT_DOMAIN;
    }

    public static byte[] getDefaultTargetInformation() {
        return DEFAULT_TARGET_INFORMATION;
    }

    private void parse(byte[] material) throws IOException {
        for (int i = 0; i < 8; i++) {
            if (material[i] != NTLMSSP_SIGNATURE[i]) {
                throw new IOException("Not an NTLMSSP message.");
            }
        }
        if (readULong(material, 8) != 2) {
            throw new IOException("Not a Type 2 message.");
        }
        int flags = readULong(material, 20);
        setFlags(flags);
        String target = null;
        byte[] bytes = readSecurityBuffer(material, 12);
        if (bytes.length != 0) {
            target = new String(bytes,
                    ((flags & NTLMSSP_NEGOTIATE_UNICODE) != 0) ?
                            UNI_ENCODING : getOEMEncoding());
        }
        setTarget(target);
        for (int i = 24; i < 32; i++) {
            if (material[i] != 0) {
                byte[] challenge = new byte[8];
                System.arraycopy(material, 24, challenge, 0, 8);
                setChallenge(challenge);
                break;
            }
        }
        int offset = readULong(material, 16); // offset of targetname start
        if (offset == 32 || material.length == 32) return;
        for (int i = 32; i < 40; i++) {
            if (material[i] != 0) {
                byte[] context = new byte[8];
                System.arraycopy(material, 32, context, 0, 8);
                setContext(context);
                break;
            }
        }
        if (offset == 40 || material.length == 40) return;
        bytes = readSecurityBuffer(material, 40);
        if (bytes.length != 0) setTargetInformation(bytes);
    }

}
