/** * Copyright (c) 2009, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * 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.apache.synapse.transport.passthru; import org.apache.axiom.om.OMOutputFormat; import org.apache.axis2.AxisFault; import org.apache.axis2.context.MessageContext; import org.apache.axis2.transport.MessageFormatter; import org.apache.axis2.util.MessageProcessorSelector; import org.apache.http.ConnectionReuseStrategy; import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.HttpVersion; import org.apache.http.ProtocolVersion; import org.apache.http.entity.BasicHttpEntity; import org.apache.http.impl.DefaultConnectionReuseStrategy; import org.apache.http.nio.ContentEncoder; import org.apache.http.nio.NHttpServerConnection; import org.apache.http.params.DefaultedHttpParams; import org.apache.http.protocol.ExecutionContext; import org.apache.http.protocol.HTTP; import org.apache.synapse.transport.passthru.config.SourceConfiguration; import org.apache.synapse.transport.passthru.util.PassThroughTransportUtils; import org.apache.synapse.transport.passthru.util.RelayUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.TreeSet; public class SourceResponse { private Pipe pipe = null; /** Transport headers */ private Map<String, TreeSet<String>> headers = new HashMap<String, TreeSet<String>>(); /** Status of the response */ private int status = HttpStatus.SC_OK; /** Status line */ private String statusLine = null; /** Actual response submitted */ private HttpResponse response = null; /** Configuration of the receiver */ private SourceConfiguration sourceConfiguration; /** Version of the response */ private ProtocolVersion version = HttpVersion.HTTP_1_1; /** Connection strategy */ private ConnectionReuseStrategy connStrategy = new DefaultConnectionReuseStrategy(); /** Chunk response or not */ // private boolean chunk = true; /** response has an entity or not**/ private boolean hasEntity = true; /** Keep alive request */ private boolean keepAlive = true; private SourceRequest request = null; /** If version change required default HTTP 1.1 will be overridden*/ private boolean versionChangeRequired =false; public SourceResponse(SourceConfiguration config, int status, SourceRequest request) { this(config, status, null, request); } public SourceResponse(SourceConfiguration config, int status, String statusLine, SourceRequest request) { this.status = status; this.statusLine = statusLine; this.sourceConfiguration = config; this.request = request; if (request != null && request.getVersion() != null) { this.version = request.getVersion(); } } public void connect(Pipe pipe) { this.pipe = pipe; if (request != null && pipe != null) { SourceContext.get(request.getConnection()).setWriter(pipe); } } /** * Starts the response by writing the headers * @param conn connection * @throws java.io.IOException if an error occurs * @throws org.apache.http.HttpException if an error occurs */ public void start(NHttpServerConnection conn) throws IOException, HttpException { // create the response response = sourceConfiguration.getResponseFactory().newHttpResponse( request.getVersion(), this.status, request.getConnection().getContext()); if (statusLine != null) { response.setStatusLine(version, status, statusLine); } else if (versionChangeRequired){ response.setStatusLine(version, status); } else { response.setStatusCode(status); } BasicHttpEntity entity = null; if (canResponseHaveBody(request.getRequest(), response)) { entity = new BasicHttpEntity(); long contentLength = -1; String contentLengthHeader = null; if (headers.get(HTTP.CONTENT_LEN) != null && headers.get(HTTP.CONTENT_LEN).size() > 0) { contentLengthHeader = headers.get(HTTP.CONTENT_LEN).first(); } if (contentLengthHeader != null) { contentLength = Long.parseLong(contentLengthHeader); headers.remove(HTTP.CONTENT_LEN); } if (contentLength != -1) { entity.setChunked(false); entity.setContentLength(contentLength); } else { entity.setChunked(true); } } response.setEntity(entity); // set any transport headers Set<Map.Entry<String, TreeSet<String>>> entries = headers.entrySet(); for (Map.Entry<String, TreeSet<String>> entry : entries) { if (entry.getKey() != null) { Iterator<String> i = entry.getValue().iterator(); while(i.hasNext()) { response.addHeader(entry.getKey(), i.next()); } } } if (!keepAlive) { response.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE); } response.setParams(new DefaultedHttpParams(response.getParams(), sourceConfiguration.getHttpParams())); SourceContext.updateState(conn, ProtocolState.RESPONSE_HEAD); // Pre-process HTTP response conn.getContext().setAttribute(ExecutionContext.HTTP_CONNECTION, conn); conn.getContext().setAttribute(ExecutionContext.HTTP_RESPONSE, response); conn.getContext().setAttribute(ExecutionContext.HTTP_REQUEST, SourceContext.getRequest(conn).getRequest()); sourceConfiguration.getHttpProcessor().process(response, conn.getContext()); //Since HTTP HEAD request doesn't contain message body, entity will be null when above HttpProcessor() // process invoked. Hence content length is set to 0 inside the HTTP-Core. To avoid that content length of // the backend response is set as the content length. if (entity == null && PassThroughConstants.HTTP_HEAD.equalsIgnoreCase(request.getRequest().getRequestLine().getMethod())) { if (response.getFirstHeader(PassThroughConstants.ORGINAL_CONTEN_LENGTH) == null && response.getFirstHeader( HTTP.CONTENT_LEN).getValue() != null && (response.getFirstHeader(HTTP.CONTENT_LEN).getValue().equals("0"))) { response.removeHeaders(HTTP.CONTENT_LEN); } else { response.removeHeaders(HTTP.CONTENT_LEN); response.addHeader(HTTP.CONTENT_LEN, response.getFirstHeader(PassThroughConstants.ORGINAL_CONTEN_LENGTH).getValue()); response.removeHeaders(PassThroughConstants.ORGINAL_CONTEN_LENGTH); } } conn.submitResponse(response); // Handle non entity body responses if (entity == null) { hasEntity = false; // Reset connection state sourceConfiguration.getSourceConnections().releaseConnection(conn); // Make ready to deal with a new request conn.requestInput(); } } public void checkResponseChunkDisable(MessageContext responseMsgContext) throws IOException { if (responseMsgContext.getProperty(PassThroughConstants.HTTP_SC) != null){ if (this.canResponseHaveContentLength(responseMsgContext)) { calculateContentlengthForChunckDisabledResponse(responseMsgContext); } } else { calculateContentlengthForChunckDisabledResponse(responseMsgContext); } } /** * Calculates the content-length when chunking is disabled. * @param responseMsgContext outflow message context * @throws IOException */ private void calculateContentlengthForChunckDisabledResponse(MessageContext responseMsgContext) throws IOException { String forceHttp10 = (String) responseMsgContext.getProperty(PassThroughConstants.FORCE_HTTP_1_0); if ("true".equals(forceHttp10) || responseMsgContext.isPropertyTrue(PassThroughConstants.DISABLE_CHUNKING, false)) { if (!responseMsgContext.isPropertyTrue(PassThroughConstants.MESSAGE_BUILDER_INVOKED, false)) { try { RelayUtils.buildMessage(responseMsgContext, false); responseMsgContext.getEnvelope().buildWithAttachments(); } catch (Exception e) { throw new AxisFault(e.getMessage()); } } if("true".equals(forceHttp10)){ version = HttpVersion.HTTP_1_0; versionChangeRequired=true; } Boolean noEntityBody = (Boolean) responseMsgContext.getProperty(PassThroughConstants.NO_ENTITY_BODY); if (noEntityBody != null && Boolean.TRUE == noEntityBody) { headers.remove(HTTP.CONTENT_TYPE); return; } MessageFormatter formatter = MessageProcessorSelector.getMessageFormatter(responseMsgContext); OMOutputFormat format = PassThroughTransportUtils.getOMOutputFormat(responseMsgContext); ByteArrayOutputStream out = new ByteArrayOutputStream(); formatter.writeTo(responseMsgContext, format, out, false); TreeSet<String> header = new TreeSet<String>(); header.add(String.valueOf(out.toByteArray().length)); headers.put(HTTP.CONTENT_LEN, header); } } /** * Consume the content through the Pipe and write them to the wire * @param conn connection * @param encoder encoder * @throws java.io.IOException if an error occurs * @return number of bytes written */ public int write(NHttpServerConnection conn, ContentEncoder encoder) throws IOException { int bytes = 0; if (pipe != null) { bytes = pipe.consume(encoder); } else { encoder.complete(); } // Update connection state if (encoder.isCompleted()) { SourceContext.updateState(conn, ProtocolState.RESPONSE_DONE); sourceConfiguration.getMetrics(). notifySentMessageSize(conn.getMetrics().getSentBytesCount()); if (response != null && !this.connStrategy.keepAlive(response, conn.getContext())) { SourceContext.updateState(conn, ProtocolState.CLOSING); sourceConfiguration.getSourceConnections().closeConnection(conn); } else if (SourceContext.get(conn).isShutDown()) { // we need to shut down if the shutdown flag is set SourceContext.updateState(conn, ProtocolState.CLOSING); sourceConfiguration.getSourceConnections().closeConnection(conn); } else { // Reset connection state sourceConfiguration.getSourceConnections().releaseConnection(conn); // Ready to deal with a new request conn.requestInput(); } } return bytes; } public void addHeader(String name, String value) { if(headers.get(name) == null) { TreeSet<String> values = new TreeSet<String>(); values.add(value); headers.put(name, values); } else { TreeSet<String> values = headers.get(name); values.add(value); } } public void setStatus(int status) { this.status = status; } public void removeHeader(String name) { if (headers.get(name) != null) { headers.remove(name); } } public String getHeader(String name) { if (headers.containsKey(name)){ return headers.get(name).first(); } return null; } private boolean canResponseHaveBody(final HttpRequest request, final HttpResponse response) { if (request != null && "HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) { return false; } int status = response.getStatusLine().getStatusCode(); return status >= HttpStatus.SC_OK && status != HttpStatus.SC_NO_CONTENT && status != HttpStatus.SC_NOT_MODIFIED && status != HttpStatus.SC_RESET_CONTENT; } /** * Checks whether response can have Content-Length header * @param responseMsgContext out flow message context * @return true if response can have Content-Length header else false */ private boolean canResponseHaveContentLength(MessageContext responseMsgContext) { Object httpStatus = responseMsgContext.getProperty(PassThroughConstants.HTTP_SC); int status; if (httpStatus == null || httpStatus.toString().equals("")) { return false; } if (httpStatus instanceof String) { status = Integer.parseInt((String)httpStatus); } else { status = (Integer) httpStatus; } if (request != null && PassThroughConstants.HTTP_CONNECT.equals(request.getRequest().getRequestLine() .getMethod())) { return (status / 100 != 2); } else { return HttpStatus.SC_NO_CONTENT != status && (status / 100 != 1); } } public boolean hasEntity() { return this.hasEntity; } public void setKeepAlive(boolean keepAlive) { this.keepAlive = keepAlive; } }