001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *  
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *  
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License. 
018 *  
019 */
020package org.apache.mina.http;
021
022import java.nio.ByteBuffer;
023import java.util.HashMap;
024import java.util.Map;
025import java.util.regex.Pattern;
026
027import org.apache.mina.core.buffer.IoBuffer;
028import org.apache.mina.core.session.IoSession;
029import org.apache.mina.filter.codec.ProtocolDecoder;
030import org.apache.mina.filter.codec.ProtocolDecoderOutput;
031import org.apache.mina.http.api.DefaultHttpResponse;
032import org.apache.mina.http.api.HttpEndOfContent;
033import org.apache.mina.http.api.HttpStatus;
034import org.apache.mina.http.api.HttpVersion;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038public class HttpClientDecoder implements ProtocolDecoder {
039    private static final Logger LOG = LoggerFactory.getLogger(HttpClientCodec.class);
040
041    /** Key for decoder current state */
042    private static final String DECODER_STATE_ATT = "http.ds";
043
044    /** Key for the partial HTTP requests head */
045    private static final String PARTIAL_HEAD_ATT = "http.ph";
046
047    /** Key for the number of bytes remaining to read for completing the body */
048    private static final String BODY_REMAINING_BYTES = "http.brb";
049    
050    /** Key for indicating chunked data */
051    private static final String BODY_CHUNKED = "http.ckd";
052    
053    /** Regex to parse HttpRequest Request Line */
054    public static final Pattern REQUEST_LINE_PATTERN = Pattern.compile(" ");
055
056    /** Regex to parse HttpRequest Request Line */
057    public static final Pattern RESPONSE_LINE_PATTERN = Pattern.compile(" ");
058
059    /** Regex to parse out QueryString from HttpRequest */
060    public static final Pattern QUERY_STRING_PATTERN = Pattern.compile("\\?");
061
062    /** Regex to parse out parameters from query string */
063    public static final Pattern PARAM_STRING_PATTERN = Pattern.compile("\\&|;");
064
065    /** Regex to parse out key/value pairs */
066    public static final Pattern KEY_VALUE_PATTERN = Pattern.compile("=");
067
068    /** Regex to parse raw headers and body */
069    public static final Pattern RAW_VALUE_PATTERN = Pattern.compile("\\r\\n\\r\\n");
070
071    /** Regex to parse raw headers from body */
072    public static final Pattern HEADERS_BODY_PATTERN = Pattern.compile("\\r\\n");
073
074    /** Regex to parse header name and value */
075    public static final Pattern HEADER_VALUE_PATTERN = Pattern.compile(": ");
076
077    /** Regex to split cookie header following RFC6265 Section 5.4 */
078    public static final Pattern COOKIE_SEPARATOR_PATTERN = Pattern.compile(";");
079
080    public void decode(final IoSession session, final IoBuffer msg, final ProtocolDecoderOutput out) {
081        DecoderState state = (DecoderState)session.getAttribute(DECODER_STATE_ATT);
082        if (null == state) {
083            session.setAttribute(DECODER_STATE_ATT, DecoderState.NEW);
084            state = (DecoderState)session.getAttribute(DECODER_STATE_ATT);
085        }
086        switch (state) {
087        case HEAD:
088            LOG.debug("decoding HEAD");
089            // grab the stored a partial HEAD request
090            final ByteBuffer oldBuffer = (ByteBuffer)session.getAttribute(PARTIAL_HEAD_ATT);
091            // concat the old buffer and the new incoming one
092            IoBuffer.allocate(oldBuffer.remaining() + msg.remaining()).put(oldBuffer).put(msg).flip();
093            // now let's decode like it was a new message
094
095        case NEW:
096            LOG.debug("decoding NEW");
097            final DefaultHttpResponse rp = parseHttpReponseHead(msg.buf());
098
099            if (rp == null) {
100                // we copy the incoming BB because it's going to be recycled by the inner IoProcessor for next reads
101                final ByteBuffer partial = ByteBuffer.allocate(msg.remaining());
102                partial.put(msg.buf());
103                partial.flip();
104                // no request decoded, we accumulate
105                session.setAttribute(PARTIAL_HEAD_ATT, partial);
106                session.setAttribute(DECODER_STATE_ATT, DecoderState.HEAD);
107            } else {
108                out.write(rp);
109                // is it a response with some body content ?
110                LOG.debug("response with content");
111                session.setAttribute(DECODER_STATE_ATT, DecoderState.BODY);
112
113                final String contentLen = rp.getHeader("content-length");
114
115                if (contentLen != null) {
116                    LOG.debug("found content len : {}", contentLen);
117                    session.setAttribute(BODY_REMAINING_BYTES, Integer.valueOf(contentLen));
118                } else if ("chunked".equalsIgnoreCase(rp.getHeader("transfer-encoding"))) {
119                    LOG.debug("no content len but chunked");
120                    session.setAttribute(BODY_CHUNKED, Boolean.TRUE);
121                } else if ("close".equalsIgnoreCase(rp.getHeader("connection"))) {
122                    session.closeNow();
123                } else {
124                    throw new HttpException(HttpStatus.CLIENT_ERROR_LENGTH_REQUIRED, "no content length !");
125                }
126            }
127
128            break;
129
130        case BODY:
131            LOG.debug("decoding BODY: {} bytes", msg.remaining());
132            final int chunkSize = msg.remaining();
133            // send the chunk of body
134            if (chunkSize != 0) {
135                final IoBuffer wb = IoBuffer.allocate(msg.remaining());
136                wb.put(msg);
137                wb.flip();
138                out.write(wb);
139            }
140            msg.position(msg.limit());
141            
142            // do we have reach end of body ?
143            int remaining = 0;
144            
145            // if chunked, remaining is the msg.remaining()
146            if( session.getAttribute(BODY_CHUNKED) != null ) {
147                remaining = chunkSize;
148            } else {
149                // otherwise, manage with content-length
150                remaining = (Integer) session.getAttribute(BODY_REMAINING_BYTES);
151                remaining -= chunkSize;
152            }
153
154            if (remaining <= 0 ) {
155                LOG.debug("end of HTTP body");
156                session.setAttribute(DECODER_STATE_ATT, DecoderState.NEW);
157                session.removeAttribute(BODY_REMAINING_BYTES);
158                if( session.getAttribute(BODY_CHUNKED) != null ) {
159                    session.removeAttribute(BODY_CHUNKED);
160                }
161                out.write(new HttpEndOfContent());
162            } else {
163                if( session.getAttribute(BODY_CHUNKED) == null ) {
164                    session.setAttribute(BODY_REMAINING_BYTES, Integer.valueOf(remaining));
165                }
166            }
167
168            break;
169
170        default:
171            throw new HttpException(HttpStatus.SERVER_ERROR_INTERNAL_SERVER_ERROR, "Unknonwn decoder state : " + state);
172        }
173    }
174
175    public void finishDecode(final IoSession session, final ProtocolDecoderOutput out) throws Exception {
176    }
177
178    public void dispose(final IoSession session) throws Exception {
179    }
180
181    private DefaultHttpResponse parseHttpReponseHead(final ByteBuffer buffer) {
182        // Java 6 >> String raw = new String(buffer.array(), 0, buffer.limit(), Charset.forName("UTF-8"));
183        final String raw = new String(buffer.array(), 0, buffer.limit());
184        final String[] headersAndBody = RAW_VALUE_PATTERN.split(raw, -1);
185        if (headersAndBody.length <= 1) {
186            // we didn't receive the full HTTP head
187            return null;
188        }
189
190        String[] headerFields = HEADERS_BODY_PATTERN.split(headersAndBody[0]);
191        headerFields = ArrayUtil.dropFromEndWhile(headerFields, "");
192
193        final String requestLine = headerFields[0];
194        final Map<String, String> generalHeaders = new HashMap<String, String>();
195
196        for (int i = 1; i < headerFields.length; i++) {
197            final String[] header = HEADER_VALUE_PATTERN.split(headerFields[i]);
198            generalHeaders.put(header[0].toLowerCase(), header[1]);
199        }
200
201        final String[] elements = RESPONSE_LINE_PATTERN.split(requestLine);
202        HttpStatus status = null;
203        final int statusCode = Integer.valueOf(elements[1]);
204        for (int i = 0; i < HttpStatus.values().length; i++) {
205            status = HttpStatus.values()[i];
206            if (statusCode == status.code()) {
207                break;
208            }
209        }
210        final HttpVersion version = HttpVersion.fromString(elements[0]);
211
212        // we put the buffer position where we found the beginning of the HTTP body
213        buffer.position(headersAndBody[0].length() + 4);
214
215        return new DefaultHttpResponse(version, status, generalHeaders);
216    }
217}