/*
 * Copyright 2013 - 2013 Cole Wen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.maodian.flycat.xmpp;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.maodian.flycat.holder.XMLInputFactoryHolder;
import org.maodian.flycat.xmpp.OpeningStreamState.FeatureType;

/**
 * @author Cole Wen
 *
 */
public class SASLState implements State {
  private static final String SUCCESS_RESPONSE = "<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>";
  private StringBuilder cachedXML = new StringBuilder();
  private int readCount = 0;

  /* (non-Javadoc)
   * @see org.maodian.flycat.xmpp.State#handle(org.maodian.flycat.xmpp.XmppContext, java.lang.String)
   */
  @Override
  public String handle(XmppContext context, String xml) {
    // since we use '>' as delimiter in the DelimiterBasedFrameDecoder instance,
    // the whole <auth /> node should be transmited completely at the sencond time.
    if (readCount++ < 1) {
      cachedXML.append(xml);
      return "";
    }
    cachedXML.append(xml);
    try (Reader reader = new StringReader(cachedXML.toString())) {
      try {
        XMLStreamReader xmlsr = XMLInputFactoryHolder.getXMLInputFactory().createXMLStreamReader(reader);
        xmlsr.nextTag();
        QName qname = new QName(XmppNamespace.SASL, "auth");
        if (!qname.equals(xmlsr.getName())) {
          throw new XmppException(StreamError.INVALID_NAMESPACE)
              .set("QName", xmlsr.getName());
        }
        
        String mechanism = xmlsr.getAttributeValue("", "mechanism");
        if (StringUtils.equals("PLAIN", mechanism)) {
          String base64Data = xmlsr.getElementText();
          byte[] value = Base64.decodeBase64(base64Data);
          String text = new String(value, StandardCharsets.UTF_8);
          
          // apply PLAIN SASL mechanism whose rfc locates at http://tools.ietf.org/html/rfc4616
          int[] nullPosition = {-1, -1};
          int nullIndex = 0;
          for (int i = 0; i < text.length(); ++i) {
            if (text.codePointAt(i) == 0) {
              nullPosition[nullIndex++] = i;
            }
          }
          
          if (nullPosition[0] == -1 || nullPosition[1] == -1) {
            throw new RuntimeException("The format is invalid");
          }
          String authzid = StringUtils.substring(text, 0, nullPosition[0]);
          String authcid = StringUtils.substring(text, nullPosition[0] + 1, nullPosition[1]);
          String password = StringUtils.substring(text, nullPosition[1] + 1);
          
          if (authzid.getBytes(StandardCharsets.UTF_8).length > 255 || authcid.getBytes(StandardCharsets.UTF_8).length > 255
              || password.getBytes(StandardCharsets.UTF_8).length > 255) {
            throw new RuntimeException("authorization id, authentication id and password should be equal or less than 255 bytes");
          }
          
          context.setBareJID(authcid + "@localhost");
          context.setState(new OpeningStreamState(FeatureType.RESOURCE_BIND));
          return SUCCESS_RESPONSE;
        } else {
          //TODO: deal with unsupported mechanism
          throw new RuntimeException("Unsupported mechanism");
        }
        
      } catch (XMLStreamException e) {
        throw new XmppException(e, StreamError.BAD_FORMAT);
      }
    } catch (IOException ioe) {
      // close a StringReader/StringWriter should not cause IOException, though
      throw new XmppException(ioe, StreamError.INTERNAL_SERVER_ERROR);
    }
  }

}

@@ -2433,24 +2433,171 @@
 mentText();%0A
+          %0A          if (!Base64.isBase64(base64Data)) %7B%0A            throw new XmppException(SASLError.INCORRECT_ENCODING);%0A          %7D%0A          %0A
           by
@@ -3142,39 +3142,36 @@
       throw new 
-Runtime
+Xmpp
 Exception(%22The f
@@ -3187,16 +3187,45 @@
 invalid%22
+, SASLError.MALFORMED_REQUEST
 );%0A     
@@ -3704,39 +3704,36 @@
       throw new 
-Runtime
+Xmpp
 Exception(%22autho
@@ -3815,16 +3815,62 @@
 5 bytes%22
+, %0A                SASLError.MALFORMED_REQUEST
 );%0A     
@@ -4076,58 +4076,8 @@
 e %7B%0A
-          //TODO: deal with unsupported mechanism%0A
     
@@ -4096,15 +4096,12 @@
 new 
-Runtime
+Xmpp
 Exce
@@ -4110,20 +4110,53 @@
 ion(
-%22Unsupported
+SASLError.INVALID_MECHANISM).set(%22mechanism%22,
  mec
@@ -4153,33 +4153,32 @@
 nism%22, mechanism
-%22
 );%0A        %7D%0A   
@@ -4262,29 +4262,31 @@
 e, S
-tream
+ASL
 Error.
-BAD_FORMAT
+NOT_AUTHORIZED
 );%0A 
@@ -4444,40 +4444,31 @@
 e, S
-tream
+ASL
 Error.
-INTERNAL_SERVER_ERROR
+NOT_AUTHORIZED
 );%0A 

apply approriate SASLError