001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.activemq.transport.http; 018 019import java.io.BufferedReader; 020import java.io.DataOutputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.InputStreamReader; 024import java.util.HashMap; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.concurrent.ConcurrentMap; 027import java.util.concurrent.LinkedBlockingQueue; 028import java.util.concurrent.TimeUnit; 029import java.util.zip.GZIPInputStream; 030import javax.servlet.ServletException; 031import javax.servlet.http.HttpServlet; 032import javax.servlet.http.HttpServletRequest; 033import javax.servlet.http.HttpServletResponse; 034 035import org.apache.activemq.Service; 036import org.apache.activemq.command.Command; 037import org.apache.activemq.command.ConnectionInfo; 038import org.apache.activemq.command.WireFormatInfo; 039import org.apache.activemq.transport.Transport; 040import org.apache.activemq.transport.TransportAcceptListener; 041import org.apache.activemq.transport.util.TextWireFormat; 042import org.apache.activemq.transport.xstream.XStreamWireFormat; 043import org.apache.activemq.util.IOExceptionSupport; 044import org.apache.activemq.util.ServiceListener; 045import org.slf4j.Logger; 046import org.slf4j.LoggerFactory; 047 048/** 049 * A servlet which handles server side HTTP transport, delegating to the 050 * ActiveMQ broker. This servlet is designed for being embedded inside an 051 * ActiveMQ Broker using an embedded Jetty or Tomcat instance. 052 */ 053public class HttpTunnelServlet extends HttpServlet { 054 private static final long serialVersionUID = -3826714430767484333L; 055 private static final Logger LOG = LoggerFactory.getLogger(HttpTunnelServlet.class); 056 057 private TransportAcceptListener listener; 058 private HttpTransportFactory transportFactory; 059 private TextWireFormat wireFormat; 060 private ConcurrentMap<String, BlockingQueueTransport> clients = new ConcurrentHashMap<String, BlockingQueueTransport>(); 061 private final long requestTimeout = 30000L; 062 private HashMap<String, Object> transportOptions; 063 private HashMap<String, Object> wireFormatOptions; 064 065 @SuppressWarnings("unchecked") 066 @Override 067 public void init() throws ServletException { 068 super.init(); 069 listener = (TransportAcceptListener)getServletContext().getAttribute("acceptListener"); 070 if (listener == null) { 071 throw new ServletException("No such attribute 'acceptListener' available in the ServletContext"); 072 } 073 transportFactory = (HttpTransportFactory)getServletContext().getAttribute("transportFactory"); 074 if (transportFactory == null) { 075 throw new ServletException("No such attribute 'transportFactory' available in the ServletContext"); 076 } 077 transportOptions = (HashMap<String, Object>)getServletContext().getAttribute("transportOptions"); 078 wireFormatOptions = (HashMap<String, Object>)getServletContext().getAttribute("wireFormatOptions"); 079 wireFormat = (TextWireFormat)getServletContext().getAttribute("wireFormat"); 080 if (wireFormat == null) { 081 wireFormat = createWireFormat(); 082 } 083 } 084 085 @Override 086 protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 087 response.addHeader("Accepts-Encoding", "gzip"); 088 super.doOptions(request, response); 089 } 090 091 @Override 092 protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 093 createTransportChannel(request, response); 094 } 095 096 @Override 097 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 098 // lets return the next response 099 Command packet = null; 100 int count = 0; 101 try { 102 BlockingQueueTransport transportChannel = getTransportChannel(request, response); 103 if (transportChannel == null) { 104 return; 105 } 106 107 packet = (Command)transportChannel.getQueue().poll(requestTimeout, TimeUnit.MILLISECONDS); 108 109 DataOutputStream stream = new DataOutputStream(response.getOutputStream()); 110 wireFormat.marshal(packet, stream); 111 count++; 112 } catch (InterruptedException ignore) { 113 } 114 115 if (count == 0) { 116 response.setStatus(HttpServletResponse.SC_REQUEST_TIMEOUT); 117 } 118 } 119 120 @Override 121 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 122 123 if (wireFormatOptions.get("maxFrameSize") != null && request.getContentLength() > Integer.parseInt(wireFormatOptions.get("maxFrameSize").toString())) { 124 throw new ServletException("maxFrameSize exceeded"); 125 } 126 127 InputStream stream = request.getInputStream(); 128 String contentType = request.getContentType(); 129 if (contentType != null && contentType.equals("application/x-gzip")) { 130 stream = new GZIPInputStream(stream); 131 } 132 133 // Read the command directly from the reader, assuming UTF8 encoding 134 Command command = (Command) wireFormat.unmarshalText(new InputStreamReader(stream, "UTF-8")); 135 136 if (command instanceof WireFormatInfo) { 137 WireFormatInfo info = (WireFormatInfo) command; 138 if (!canProcessWireFormatVersion(info.getVersion())) { 139 response.sendError(HttpServletResponse.SC_NOT_FOUND, "Cannot process wire format of version: " 140 + info.getVersion()); 141 } 142 143 } else { 144 145 BlockingQueueTransport transport = getTransportChannel(request, response); 146 if (transport == null) { 147 return; 148 } 149 150 if (command instanceof ConnectionInfo) { 151 ((ConnectionInfo) command).setTransportContext(request.getAttribute("javax.servlet.request.X509Certificate")); 152 } 153 transport.doConsume(command); 154 } 155 } 156 157 private boolean canProcessWireFormatVersion(int version) { 158 return true; 159 } 160 161 protected String readRequestBody(HttpServletRequest request) throws IOException { 162 StringBuffer buffer = new StringBuffer(); 163 BufferedReader reader = request.getReader(); 164 while (true) { 165 String line = reader.readLine(); 166 if (line == null) { 167 break; 168 } else { 169 buffer.append(line); 170 buffer.append("\n"); 171 } 172 } 173 return buffer.toString(); 174 } 175 176 protected BlockingQueueTransport getTransportChannel(HttpServletRequest request, HttpServletResponse response) throws IOException { 177 String clientID = request.getHeader("clientID"); 178 if (clientID == null) { 179 response.sendError(HttpServletResponse.SC_BAD_REQUEST, "No clientID header specified"); 180 LOG.warn("No clientID header specified"); 181 return null; 182 } 183 BlockingQueueTransport answer = clients.get(clientID); 184 if (answer == null) { 185 LOG.warn("The clientID header specified is invalid. Client sesion has not yet been established for it: " + clientID); 186 return null; 187 } 188 return answer; 189 } 190 191 protected BlockingQueueTransport createTransportChannel(HttpServletRequest request, HttpServletResponse response) throws IOException { 192 final String clientID = request.getHeader("clientID"); 193 194 if (clientID == null) { 195 response.sendError(HttpServletResponse.SC_BAD_REQUEST, "No clientID header specified"); 196 LOG.warn("No clientID header specified"); 197 return null; 198 } 199 200 // Optimistically create the client's transport; this transport may be thrown away if the client has already registered. 201 BlockingQueueTransport answer = createTransportChannel(); 202 203 // Record the client's transport and ensure that it has not already registered; this is thread-safe and only allows one 204 // thread to register the client 205 if (clients.putIfAbsent(clientID, answer) != null) { 206 response.sendError(HttpServletResponse.SC_BAD_REQUEST, "A session for the given clientID has already been established"); 207 LOG.warn("A session for clientID '" + clientID + "' has already been established"); 208 return null; 209 } 210 211 // Ensure that the client's transport is cleaned up when no longer 212 // needed. 213 answer.addServiceListener(new ServiceListener() { 214 public void started(Service service) { 215 // Nothing to do. 216 } 217 218 public void stopped(Service service) { 219 clients.remove(clientID); 220 } 221 }); 222 223 // Configure the transport with any additional properties or filters. Although the returned transport is not explicitly 224 // persisted, if it is a filter (e.g., InactivityMonitor) it will be linked to the client's transport as a TransportListener 225 // and not GC'd until the client's transport is disposed. 226 Transport transport = answer; 227 try { 228 // Preserve the transportOptions for future use by making a copy before applying (they are removed when applied). 229 HashMap<String, Object> options = new HashMap<String, Object>(transportOptions); 230 transport = transportFactory.serverConfigure(answer, null, options); 231 } catch (Exception e) { 232 throw IOExceptionSupport.create(e); 233 } 234 235 // Wait for the transport to be connected or disposed. 236 listener.onAccept(transport); 237 while (!transport.isConnected() && !transport.isDisposed()) { 238 try { 239 Thread.sleep(100); 240 } catch (InterruptedException ignore) { 241 } 242 } 243 244 // Ensure that the transport was not prematurely disposed. 245 if (transport.isDisposed()) { 246 response.sendError(HttpServletResponse.SC_BAD_REQUEST, "The session for the given clientID was prematurely disposed"); 247 LOG.warn("The session for clientID '" + clientID + "' was prematurely disposed"); 248 return null; 249 } 250 251 return answer; 252 } 253 254 protected BlockingQueueTransport createTransportChannel() { 255 return new BlockingQueueTransport(new LinkedBlockingQueue<Object>()); 256 } 257 258 protected TextWireFormat createWireFormat() { 259 return new XStreamWireFormat(); 260 } 261}