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.close(true); 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}