package org.maachang.proxy.engine.net.http;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

import org.maachang.proxy.engine.net.RequestInfo;
import org.maachang.util.AnalysisUtil;
import org.maachang.util.StringUtil;

/**
 * Http受信処理.
 * <BR>
 * Httpリクエスト情報を構成するための処理です.
 * 
 * @version 2008/02/08
 * @author masahito suzuki
 * @since MaachangProxy 1.00
 */
public class ReadHttpdRequest {
    
    private static final String CONTENT_LENGTH = "Content-Length" ;
    private static final String TRANSFER_ENCODING = "Transfer-Encoding" ;
    private static final String CHUNKED = "chunked" ;
    private static final String CHARSET = "UTF8" ;
    private static final byte[] ENTER = new byte[] {(byte) 13, (byte) 10} ;
    private static final byte[] END_HEADER = new byte[] {
        (byte) 0x0d, (byte) 0x0a,(byte) 0x0d, (byte) 0x0a} ;
    
    private ReadHttpdRequest() {
        
    }
    
    /**
     * Httpリクエスト受信処理.
     * <BR><BR>
     * Httpリクエスト受信処理を実施します.
     * <BR>
     * @param stream Socket.getInputStreamを設定します.
     * @param seqId 対象のシーケンスIDを設定します.
     * @return RequestInfo RequestInfoオブジェクトが返されます.
     * @exception Exception 例外.
     */
    public static final RequestInfo receiveHttpRequest( InputStream stream,int seqId )
        throws Exception {
        // ヘッダを取得.
        RequestInfo ret = readHeader( stream ) ;
        
        // [POST]の場合は、コンテンツ長を取得して、Body情報を取得.
        if( "POST".equals( ret.getMethod() ) ) {
            readResponseByBody( ret,stream ) ;
        }
        return ret ;
    }
    
    /**
     * ヘッダ受信処理.
     */
    private static final RequestInfo readHeader( InputStream stream )
        throws Exception {
        int endPoint = 0 ;
        int endHeaderLen = END_HEADER.length ;
        RequestArrayBinary buf = new RequestArrayBinary() ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                if( buf.getLength() <= 0 ) {
                    return null ;
                }
                return convertHttpdHeader( buf.getBinary(),buf.getLength() ) ;
            }
            else {
                d = d & 0x000000ff ;
                buf.write( d ) ;
                if( d == ( int )( END_HEADER[ endPoint ] & 0x000000ff ) ) {
                    endPoint ++ ;
                    if( endPoint >= endHeaderLen ) {
                        if( buf.getLength() <= 0 ) {
                            return null ;
                        }
                        return convertHttpdHeader( buf.getBinary(),buf.getLength() ) ;
                    }
                }
                else {
                    endPoint = 0 ;
                }
            }
        }
    }
    
    /**
     * Body受信処理.
     */
    private static final boolean readResponseByBody( RequestInfo req,InputStream stream )
        throws Exception {
        RequestArrayBinary buf = new RequestArrayBinary() ;
        // chunkedでの受信.
        if( req.headerSize( TRANSFER_ENCODING ) == 1 ) {
            String s = req.getHeader( TRANSFER_ENCODING,0 ) ;
            if( CHUNKED.equals( s ) == false ) {
                throw new IOException( "不正な["+TRANSFER_ENCODING+"="+s+"]を検出しました" ) ;
            }
            readBodyByChunked( buf,stream ) ;
        }
        // コンテンツ長での受信.
        else if( req.headerSize( CONTENT_LENGTH ) == 1 ) {
            String s = req.getHeader( CONTENT_LENGTH,0 ) ;
            if( s == null || s.length() <= 0 ) {
                throw new IOException( "不正な["+CONTENT_LENGTH+"]を検出しました" ) ;
            }
            // コンテンツ長を取得.
            int contentLength = Integer.parseInt( s ) ;
            if( contentLength <= -1 ) {
                throw new IOException( "受信データ長["+contentLength+"]は不正です" ) ;
            }
            else if( contentLength == 0 ) {
                return false ;
            }
            readBodyByLength( buf,contentLength,stream ) ;
        }
        // 未指定の場合の受信.
        else {
            readBodyByNoSetting( buf,stream ) ;
        }
        // 取得されたバイナリをセット.
        if( buf.getLength() > 0 ) {
        	req.setBody( buf.getBinary( buf.getLength() ) ) ;
            return true ;
        }
        return false ;
    }
    
    /**
     * chunked受信.
     */
    private static final void readBodyByChunked( RequestArrayBinary buf,InputStream stream )
        throws Exception {
        int len = -1 ;
        int enterPos = 0 ;
        int pos = 0 ;
        int cnt = 0 ;
        byte[] headBuf = new byte[ 32 ] ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                throw new IOException( "不正なchunked終端を検出しました("+len+")" ) ;
            }
            d = d & 0x000000ff ;
            cnt ++ ;
            // chunkedデータ長を取得.
            if( len == -1 ) {
                // 改行を検出.
                if( d == ( int )( ENTER[ enterPos ] & 0x000000ff ) ) {
                    enterPos ++ ;
                    if( enterPos >= ENTER.length ) {
                        if( pos > 0 ) {
                            String chLen = new String( headBuf,0,pos,CHARSET ) ;
                            len = convertChunkedDataLength( chLen ) ;
                            // chunked終端.
                            if( len == 0 ) {
                                // 残りのデータが無くなるまで取得.
                                int exitCnt = 0 ;
                                for( ;; ) {
                                    if( stream.available() > 0 ) {
                                        stream.read() ;
                                        exitCnt ++ ;
                                        if( exitCnt >= ENTER.length ) {
                                            break ;
                                        }
                                    }
                                }
                                return ;
                            }
                            pos = 0 ;
                            enterPos = 0 ;
                        }
                        else {
                            enterPos = 0 ;
                        }
                    }
                }
                else if( enterPos >= 1 ) {
                    throw new IOException( "不正なchunkedデータを検出しました" ) ;
                }
                else {
                    headBuf[ pos ] = ( byte )d ;
                    pos ++ ;
                }
            }
            // データ受信.
            else {
                buf.write( d ) ;
                pos ++ ;
                if( len <= pos ) {
                    len = -1 ;
                    pos = 0 ;
                    enterPos = 0 ;
                }
            }
        }
    }
    
    /**
     * ContentLength指定の受信.
     */
    private static final void readBodyByLength( RequestArrayBinary buf,int contentLength,InputStream stream )
        throws Exception {
        int pnt = 0 ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                return ;
            }
            buf.write( d ) ;
            pnt ++ ;
            if( contentLength <= pnt ) {
                return ;
            }
        }
    }
    
    /**
     * 受信サイズ未指定の受信.
     */
    private static final void readBodyByNoSetting( RequestArrayBinary buf,InputStream stream )
        throws Exception {
        int pnt = 0 ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                return ;
            }
            buf.write( d ) ;
            pnt ++ ;
        }
    }
    
    /**
     * Chunked長を計算します.
     */
    private static final int convertChunkedDataLength( String s )
        throws Exception {
        try {
            return Integer.parseInt( s,16 ) ;
        } catch( Exception e ) {
            if( s == null || s.length() <= 0 ) {
                throw new IOException( "不正なchunkedヘッダ数値を検出しました" ) ;
            }
            throw new IOException( "不正なchunkedヘッダ数値["+s+"]を検出しました" ) ;
        }
    }
    
    /**
     * HTTPヘッダを解析.
     */
    private static final RequestInfo convertHttpdHeader( byte[] binary,int endPos )
        throws Exception {
        RequestInfo ret = new RequestInfo() ;
        int p = 0 ;
        int b = 0 ;
        int line = 0 ;
        int enterLen = ENTER.length ;
        // ヘッダ情報を読み込む.
        for( ;; ) {
            p = AnalysisUtil.binaryIndexOf( binary,ENTER,b,endPos ) ;
            if( p == -1 ) {
                if( b >= endPos ) {
                    break ;
                }
                p = endPos ;
            }
            String one = new String( binary,b,p-b,CHARSET ) ;
            b = p + enterLen ;
            
            // Method情報を取得.
            if( line == 0 ) {
                convertHttpExecutionAndVersion( ret,one ) ;
                if( ret.getVersion().endsWith( "0.9" ) == true ) {
                    // HTTPバージョンが0.9の場合.
                    return ret ;
                }
            }
            // ヘッダ解析.
            else {
                convertHttpHeader( ret,one ) ;
            }
            // ヘッダ終端を検出.
            if( p >= endPos ) {
                break ;
            }
            line ++ ;
        }
        return ret ;
    }
    /**
     * 処理タイプと、HTTPバージョンを取得.
     */
    private static final void convertHttpExecutionAndVersion( RequestInfo out,String oneLine ) {
        int pos1 = oneLine.indexOf( " " ) ;
        int pos2 = oneLine.indexOf( " ",pos1+1 ) ;
        String exec = oneLine.substring( 0,pos1 ) ;
        String url = oneLine.substring( pos1+1,pos2 ) ;
        String version = oneLine.substring( pos2+1,oneLine.length() ) ;
        out.setMethod( exec.trim() ) ;
        out.setUrl( url.trim() ) ;
        out.setVersion( version.trim() ) ;
    }
    
    /**
     * パラメータを解析.
     */
    private static final void convertHttpHeader( RequestInfo out,String oneLine ) {
        int p = oneLine.indexOf( ":" ) ;
        if( p == -1 ) {
            return ;
        }
        String key = oneLine.substring( 0,p ).trim() ;
        p += 1 ;
        oneLine = oneLine.substring( p+1,oneLine.length() ) ;
        if( oneLine == null || ( oneLine = oneLine.trim() ).length() <= 0 ) {
            out.addHeader( key,null ) ;
        }
        else if( key.indexOf( "User-Agent" ) != -1 ||
            key.indexOf( "Modified" ) != -1 ||
            key.indexOf( "Date" ) != -1 ) {
            out.putHeader( key,oneLine ) ;
        }
        else {
            if( oneLine.indexOf( "," ) != -1 ) {
                ArrayList<String> lst = StringUtil.cutString( oneLine,"," ) ;
                int len = lst.size() ;
                for( int i = 0 ; i < len ; i ++ ) {
                    if( i == 0 ) {
                        String o = lst.get( i ).trim() ;
                        if( o.length() > 0 ) {
                            int sp = o.indexOf( " " ) ;
                            if( sp != -1 ) {
                                boolean flg = false ;
                                for( int j = 0 ; j < sp ; j ++ ) {
                                    char c = o.charAt( j ) ;
                                    if( c == '\"' || c == '\'' ) {
                                        flg = true ;
                                        break ;
                                    }
                                }
                                if( flg == true ) {
                                    out.addHeader( key,o ) ;
                                }
                                else {
                                    out.addHeader( key,o.substring( 0,sp ).trim() ) ;
                                    out.addHeader( key,o.substring( sp+1 ).trim() ) ;
                                }
                            }
                            else {
                                out.addHeader( key,o ) ;
                            }
                        }
                    }
                    else {
                        String o = lst.get( i ).trim() ;
                        if( o.length() > 0 ) {
                            out.addHeader( key,o ) ;
                        }
                    }
                }
            }
            else {
                oneLine = oneLine.trim() ;
                if( oneLine.length() > 0 ) {
                    out.addHeader( key,oneLine ) ;
                }
            }
        }
    }
}

class RequestArrayBinary {
    private static final int MAX_MBYTE = 8 ;
    protected static final int MAX_LENGTH = MAX_MBYTE * 0x00100000 ;
    private static final int BUFFER = 4096 ;
    private byte[] binary = null ;
    private int length = 0 ;
    
    public RequestArrayBinary() {
        this.binary = new byte[ BUFFER ] ;
        this.length = 0 ;
    }
    
    protected void finalize() throws Exception {
        this.destroy() ;
    }
    
    public void destroy() {
        this.binary = null ;
        this.length = 0 ;
    }
    
    public void reset() {
        if( this.binary.length == BUFFER ) {
            this.length = 0 ;
        }
        else {
            this.binary = new byte[ BUFFER ] ;
            this.length = 0 ;
        }
    }
    
    public void write( int b ) throws Exception {
        if( length >= MAX_LENGTH ) {
            this.destroy() ;
            throw new IOException( "受信データ長が最大値["+MAX_LENGTH+"]を越しています" ) ;
        }
        if( binary.length <= length ) {
            byte[] t = binary ;
            int iLen = t.length * 2 ;
            if( iLen >= MAX_LENGTH ) {
                iLen = MAX_LENGTH ;
            }
            binary = new byte[ iLen ] ;
            System.arraycopy( t,0,binary,0,t.length ) ;
            t = null ;
        }
        binary[ length ] = ( byte )( b & 0x000000ff ) ;
        length ++ ;
    }
    
    public byte getByte( int no ) {
        return binary[ no ] ;
    }
    
    public byte[] getBinary() {
        return binary ;
    }
    
    public byte[] getBinary( int length ) {
        if( length <= 0 ) {
            length = this.length ;
        }
        if( this.length < length ) {
            length = this.length ;
        }
        byte[] ret = new byte[ length ] ;
        System.arraycopy( this.binary,0,ret,0,length ) ;
        return ret ;
    }
    
    public int getLength() {
        return length ;
    }
}
