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}