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.DataInputStream; 020import java.io.IOException; 021import java.io.InterruptedIOException; 022import java.net.URI; 023import java.security.cert.X509Certificate; 024import java.util.zip.GZIPInputStream; 025import java.util.zip.GZIPOutputStream; 026 027import org.apache.activemq.command.ShutdownInfo; 028import org.apache.activemq.transport.FutureResponse; 029import org.apache.activemq.transport.util.TextWireFormat; 030import org.apache.activemq.util.ByteArrayOutputStream; 031import org.apache.activemq.util.IOExceptionSupport; 032import org.apache.activemq.util.IdGenerator; 033import org.apache.activemq.util.ServiceStopper; 034import org.apache.activemq.wireformat.WireFormat; 035import org.apache.http.Header; 036import org.apache.http.HttpHost; 037import org.apache.http.HttpRequest; 038import org.apache.http.HttpRequestInterceptor; 039import org.apache.http.HttpResponse; 040import org.apache.http.HttpStatus; 041import org.apache.http.auth.AuthScope; 042import org.apache.http.auth.UsernamePasswordCredentials; 043import org.apache.http.client.CredentialsProvider; 044import org.apache.http.client.HttpClient; 045import org.apache.http.client.HttpResponseException; 046import org.apache.http.client.ResponseHandler; 047import org.apache.http.client.config.CookieSpecs; 048import org.apache.http.client.config.RequestConfig; 049import org.apache.http.client.methods.HttpGet; 050import org.apache.http.client.methods.HttpHead; 051import org.apache.http.client.methods.HttpOptions; 052import org.apache.http.client.methods.HttpPost; 053import org.apache.http.conn.HttpClientConnectionManager; 054import org.apache.http.entity.ByteArrayEntity; 055import org.apache.http.impl.client.BasicCredentialsProvider; 056import org.apache.http.impl.client.BasicResponseHandler; 057import org.apache.http.impl.client.HttpClientBuilder; 058import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 059import org.apache.http.message.AbstractHttpMessage; 060import org.apache.http.protocol.HttpContext; 061import org.apache.http.util.EntityUtils; 062import org.slf4j.Logger; 063import org.slf4j.LoggerFactory; 064 065/** 066 * A HTTP {@link org.apache.activemq.transport.Transport} which uses the 067 * <a href="http://hc.apache.org/index.html">Apache HTTP Client</a> 068 * library 069 */ 070public class HttpClientTransport extends HttpTransportSupport { 071 072 public static final int MAX_CLIENT_TIMEOUT = 90000; 073 private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransport.class); 074 private static final IdGenerator CLIENT_ID_GENERATOR = new IdGenerator(); 075 076 private HttpClient sendHttpClient; 077 private HttpClient receiveHttpClient; 078 079 private final String clientID = CLIENT_ID_GENERATOR.generateId(); 080 private boolean trace; 081 private HttpGet httpMethod; 082 private volatile int receiveCounter; 083 084 private int soTimeout = MAX_CLIENT_TIMEOUT; 085 086 private boolean useCompression = false; 087 protected boolean canSendCompressed = false; 088 private int minSendAsCompressedSize = 0; 089 090 public HttpClientTransport(TextWireFormat wireFormat, URI remoteUrl) { 091 super(wireFormat, remoteUrl); 092 } 093 094 public FutureResponse asyncRequest(Object command) throws IOException { 095 return null; 096 } 097 098 @Override 099 public void oneway(Object command) throws IOException { 100 101 if (isStopped()) { 102 throw new IOException("stopped."); 103 } 104 HttpPost httpMethod = new HttpPost(getRemoteUrl().toString()); 105 configureMethod(httpMethod); 106 String data = getTextWireFormat().marshalText(command); 107 byte[] bytes = data.getBytes("UTF-8"); 108 if (useCompression && canSendCompressed && bytes.length > minSendAsCompressedSize) { 109 ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); 110 GZIPOutputStream stream = new GZIPOutputStream(bytesOut); 111 stream.write(bytes); 112 stream.close(); 113 httpMethod.addHeader("Content-Type", "application/x-gzip"); 114 if (LOG.isTraceEnabled()) { 115 LOG.trace("Sending compressed, size = " + bytes.length + ", compressed size = " + bytesOut.size()); 116 } 117 bytes = bytesOut.toByteArray(); 118 } 119 ByteArrayEntity entity = new ByteArrayEntity(bytes); 120 httpMethod.setEntity(entity); 121 122 HttpClient client = null; 123 HttpResponse answer = null; 124 try { 125 client = getSendHttpClient(); 126 answer = client.execute(httpMethod); 127 int status = answer.getStatusLine().getStatusCode(); 128 if (status != HttpStatus.SC_OK) { 129 throw new IOException("Failed to post command: " + command + " as response was: " + answer); 130 } 131 if (command instanceof ShutdownInfo) { 132 try { 133 stop(); 134 } catch (Exception e) { 135 LOG.warn("Error trying to stop HTTP client: "+ e, e); 136 } 137 } 138 } catch (IOException e) { 139 throw IOExceptionSupport.create("Could not post command: " + command + " due to: " + e, e); 140 } finally { 141 if (answer != null) { 142 EntityUtils.consume(answer.getEntity()); 143 } 144 } 145 } 146 147 @Override 148 public Object request(Object command) throws IOException { 149 return null; 150 } 151 152 private DataInputStream createDataInputStream(HttpResponse answer) throws IOException { 153 Header encoding = answer.getEntity().getContentEncoding(); 154 if (encoding != null && "gzip".equalsIgnoreCase(encoding.getValue())) { 155 return new DataInputStream(new GZIPInputStream(answer.getEntity().getContent())); 156 } else { 157 return new DataInputStream(answer.getEntity().getContent()); 158 } 159 } 160 161 @Override 162 public void run() { 163 164 if (LOG.isTraceEnabled()) { 165 LOG.trace("HTTP GET consumer thread starting: " + this); 166 } 167 HttpClient httpClient = getReceiveHttpClient(); 168 URI remoteUrl = getRemoteUrl(); 169 170 while (!isStopped() && !isStopping()) { 171 172 httpMethod = new HttpGet(remoteUrl.toString()); 173 configureMethod(httpMethod); 174 HttpResponse answer = null; 175 176 try { 177 answer = httpClient.execute(httpMethod); 178 int status = answer.getStatusLine().getStatusCode(); 179 if (status != HttpStatus.SC_OK) { 180 if (status == HttpStatus.SC_REQUEST_TIMEOUT) { 181 LOG.debug("GET timed out"); 182 try { 183 Thread.sleep(1000); 184 } catch (InterruptedException e) { 185 onException(new InterruptedIOException()); 186 Thread.currentThread().interrupt(); 187 break; 188 } 189 } else { 190 onException(new IOException("Failed to perform GET on: " + remoteUrl + " as response was: " + answer)); 191 break; 192 } 193 } else { 194 receiveCounter++; 195 DataInputStream stream = createDataInputStream(answer); 196 Object command = getTextWireFormat().unmarshal(stream); 197 if (command == null) { 198 LOG.debug("Received null command from url: " + remoteUrl); 199 } else { 200 doConsume(command); 201 } 202 stream.close(); 203 } 204 } catch (Exception e) { // handle RuntimeException from unmarshal 205 onException(IOExceptionSupport.create("Failed to perform GET on: " + remoteUrl + " Reason: " + e.getMessage(), e)); 206 break; 207 } finally { 208 if (answer != null) { 209 try { 210 EntityUtils.consume(answer.getEntity()); 211 } catch (IOException e) { 212 } 213 } 214 } 215 } 216 } 217 218 // Properties 219 // ------------------------------------------------------------------------- 220 public HttpClient getSendHttpClient() { 221 if (sendHttpClient == null) { 222 sendHttpClient = createHttpClient(); 223 } 224 return sendHttpClient; 225 } 226 227 public void setSendHttpClient(HttpClient sendHttpClient) { 228 this.sendHttpClient = sendHttpClient; 229 } 230 231 public HttpClient getReceiveHttpClient() { 232 if (receiveHttpClient == null) { 233 receiveHttpClient = createHttpClient(); 234 } 235 return receiveHttpClient; 236 } 237 238 public void setReceiveHttpClient(HttpClient receiveHttpClient) { 239 this.receiveHttpClient = receiveHttpClient; 240 } 241 242 // Implementation methods 243 // ------------------------------------------------------------------------- 244 @Override 245 protected void doStart() throws Exception { 246 247 if (LOG.isTraceEnabled()) { 248 LOG.trace("HTTP GET consumer thread starting: " + this); 249 } 250 HttpClient httpClient = getReceiveHttpClient(); 251 URI remoteUrl = getRemoteUrl(); 252 253 HttpHead httpMethod = new HttpHead(remoteUrl.toString()); 254 configureMethod(httpMethod); 255 256 // Request the options from the server so we can find out if the broker we are 257 // talking to supports GZip compressed content. If so and useCompression is on 258 // then we can compress our POST data, otherwise we must send it uncompressed to 259 // ensure backwards compatibility. 260 HttpOptions optionsMethod = new HttpOptions(remoteUrl.toString()); 261 ResponseHandler<String> handler = new BasicResponseHandler() { 262 @Override 263 public String handleResponse(HttpResponse response) throws HttpResponseException, IOException { 264 265 for(Header header : response.getAllHeaders()) { 266 if (header.getName().equals("Accepts-Encoding") && header.getValue().contains("gzip")) { 267 LOG.info("Broker Servlet supports GZip compression."); 268 canSendCompressed = true; 269 break; 270 } 271 } 272 273 return super.handleResponse(response); 274 } 275 }; 276 277 try { 278 httpClient.execute(httpMethod, new BasicResponseHandler()); 279 httpClient.execute(optionsMethod, handler); 280 } catch(Exception e) { 281 LOG.trace("Error on start: ", e); 282 throw new IOException("Failed to perform GET on: " + remoteUrl + " as response was: " + e.getMessage()); 283 } 284 285 super.doStart(); 286 } 287 288 @Override 289 protected void doStop(ServiceStopper stopper) throws Exception { 290 if (httpMethod != null) { 291 // In some versions of the JVM a race between the httpMethod and the completion 292 // of the method when using HTTPS can lead to a deadlock. This hack attempts to 293 // detect that and interrupt the thread that's locked so that they can complete 294 // on another attempt. 295 for (int i = 0; i < 3; ++i) { 296 Thread abortThread = new Thread(new Runnable() { 297 298 @Override 299 public void run() { 300 try { 301 httpMethod.abort(); 302 } catch (Exception e) { 303 } 304 } 305 }); 306 307 abortThread.start(); 308 abortThread.join(2000); 309 if (abortThread.isAlive() && !httpMethod.isAborted()) { 310 abortThread.interrupt(); 311 } 312 } 313 } 314 } 315 316 protected HttpClient createHttpClient() { 317 HttpClientBuilder clientBuilder = HttpClientBuilder.create(); 318 clientBuilder.setConnectionManager(createClientConnectionManager()); 319 if (useCompression) { 320 clientBuilder.addInterceptorLast(new HttpRequestInterceptor() { 321 @Override 322 public void process(HttpRequest request, HttpContext context) { 323 // We expect to received a compression response that we un-gzip 324 request.addHeader("Accept-Encoding", "gzip"); 325 } 326 }); 327 } 328 329 RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); 330 if (getProxyHost() != null) { 331 HttpHost proxy = new HttpHost(getProxyHost(), getProxyPort()); 332 requestConfigBuilder.setProxy(proxy); 333 334 if (getProxyUser() != null && getProxyPassword() != null) { 335 CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); 336 credentialsProvider.setCredentials( 337 new AuthScope(getProxyHost(), getProxyPort()), 338 new UsernamePasswordCredentials(getProxyUser(), getProxyPassword())); 339 clientBuilder.setDefaultCredentialsProvider(credentialsProvider); 340 } 341 } 342 343 requestConfigBuilder.setSocketTimeout(soTimeout); 344 requestConfigBuilder.setCookieSpec(CookieSpecs.DEFAULT); 345 clientBuilder.setDefaultRequestConfig(requestConfigBuilder.build()); 346 347 return clientBuilder.build(); 348 } 349 350 protected HttpClientConnectionManager createClientConnectionManager() { 351 return new PoolingHttpClientConnectionManager(); 352 } 353 354 protected void configureMethod(AbstractHttpMessage method) { 355 method.setHeader("clientID", clientID); 356 } 357 358 public boolean isTrace() { 359 return trace; 360 } 361 362 public void setTrace(boolean trace) { 363 this.trace = trace; 364 } 365 366 @Override 367 public int getReceiveCounter() { 368 return receiveCounter; 369 } 370 371 public int getSoTimeout() { 372 return soTimeout; 373 } 374 375 public void setSoTimeout(int soTimeout) { 376 this.soTimeout = soTimeout; 377 } 378 379 public void setUseCompression(boolean useCompression) { 380 this.useCompression = useCompression; 381 } 382 383 public boolean isUseCompression() { 384 return this.useCompression; 385 } 386 387 public int getMinSendAsCompressedSize() { 388 return minSendAsCompressedSize; 389 } 390 391 /** 392 * Sets the minimum size that must be exceeded on a send before compression is used if 393 * the useCompression option is specified. For very small payloads compression can be 394 * inefficient compared to the transmission size savings. 395 * 396 * Default value is 0. 397 * 398 * @param minSendAsCompressedSize 399 */ 400 public void setMinSendAsCompressedSize(int minSendAsCompressedSize) { 401 this.minSendAsCompressedSize = minSendAsCompressedSize; 402 } 403 404 @Override 405 public X509Certificate[] getPeerCertificates() { 406 return null; 407 } 408 409 @Override 410 public void setPeerCertificates(X509Certificate[] certificates) { 411 } 412 413 @Override 414 public WireFormat getWireFormat() { 415 return getTextWireFormat(); 416 } 417 418 @Override 419 protected String getSystemPropertyPrefix() { 420 return "http."; 421 } 422 423}