package org.jgroups.auth.sasl;

import org.jgroups.Address;
import org.jgroups.Message;
import org.jgroups.protocols.SASL;
import org.jgroups.protocols.SaslHeader;
import org.jgroups.protocols.SaslHeader.Type;

import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslClient;
import javax.security.sasl.SaslClientFactory;
import javax.security.sasl.SaslException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Map;

public class SaslClientContext implements SaslContext {
    private static final byte[] EMPTY_CHALLENGE = new byte[0];
    SaslClient client;
    Subject subject;

    public SaslClientContext(final SaslClientFactory saslClientFactory, final String mech, final String server_name, final CallbackHandler callback_handler, final Map<String, String> props, final Subject subject) throws SaslException {
        this.subject = subject;
        if (this.subject != null) {
            try {
                client = Subject.doAs(this.subject, (PrivilegedExceptionAction<SaslClient>)()
                  -> saslClientFactory.createSaslClient(new String[] { mech }, null, SASL.SASL_PROTOCOL_NAME, server_name, props, callback_handler));
            } catch (PrivilegedActionException e) {
                throw (SaslException)e.getCause(); // The createSaslServer will only throw this type of exception
            }
        } else {
            client = saslClientFactory.createSaslClient(new String[] { mech }, null, SASL.SASL_PROTOCOL_NAME, server_name, props, callback_handler);
        }
    }

    @Override
    public boolean isSuccessful() {
        return client.isComplete();
    }

    @Override
    public boolean needsWrapping() {
        if (client.isComplete()) {
            String qop = (String) client.getNegotiatedProperty(Sasl.QOP);
            return (qop != null && (qop.equalsIgnoreCase("auth-int") || qop.equalsIgnoreCase("auth-conf")));
        } else {
            return false;
        }
    }

    @Override
    public byte[] wrap(byte[] outgoing, int offset, int len) throws SaslException {
        return client.wrap(outgoing, offset, len);
    }

    @Override
    public byte[] unwrap(byte[] incoming, int offset, int len) throws SaslException {
        return client.unwrap(incoming, offset, len);
    }

    @Override
    public Message nextMessage(Address address, SaslHeader header) throws SaslException {
        Message message = new Message(address).setFlag(Message.Flag.OOB);
        return addHeader(message, header.getPayload());
    }

    @Override
    public void dispose() {
        try {
            client.dispose();
        } catch (SaslException e) {
        }
    }

    public Message addHeader(Message msg, byte[] payload) throws SaslException {
        byte[] response;
        if (payload == null) {
            if (client.hasInitialResponse()) {
                response = evaluateChallenge(EMPTY_CHALLENGE);
            } else {
                response = EMPTY_CHALLENGE;
            }
        } else {
            response = evaluateChallenge(payload);
        }
        if (response != null) {
            return msg.putHeader(SASL.SASL_ID, new SaslHeader(Type.RESPONSE, response));
        } else {
            return null;
        }
    }

    private byte[] evaluateChallenge(final byte[] challenge) throws SaslException {
        if (subject != null) {
           try {
              return Subject.doAs(subject, (PrivilegedExceptionAction<byte[]>)() -> client.evaluateChallenge(challenge));
           } catch (PrivilegedActionException e) {
              Throwable cause = e.getCause();
              if (cause instanceof SaslException) {
                 throw (SaslException)cause;
              } else {
                 throw new RuntimeException(cause);
              }
           }
        } else {
           return client.evaluateChallenge(challenge);
        }
     }
}
