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     */
017    package org.apache.camel.component.jetty;
018    
019    import java.io.File;
020    import java.net.URI;
021    import java.util.HashMap;
022    import java.util.List;
023    import java.util.Map;
024    
025    import javax.management.MBeanServer;
026    
027    import org.apache.camel.CamelContext;
028    import org.apache.camel.Endpoint;
029    import org.apache.camel.RuntimeCamelException;
030    import org.apache.camel.component.http.CamelServlet;
031    import org.apache.camel.component.http.HttpBinding;
032    import org.apache.camel.component.http.HttpComponent;
033    import org.apache.camel.component.http.HttpConsumer;
034    import org.apache.camel.component.http.HttpEndpoint;
035    import org.apache.camel.spi.ManagementAgent;
036    import org.apache.camel.spi.ManagementStrategy;
037    import org.apache.camel.util.CastUtils;
038    import org.apache.camel.util.IntrospectionSupport;
039    import org.apache.camel.util.ObjectHelper;
040    import org.apache.camel.util.URISupport;
041    import org.apache.commons.logging.Log;
042    import org.apache.commons.logging.LogFactory;
043    import org.eclipse.jetty.client.Address;
044    import org.eclipse.jetty.client.HttpClient;
045    import org.eclipse.jetty.jmx.MBeanContainer;
046    import org.eclipse.jetty.server.Connector;
047    import org.eclipse.jetty.server.Handler;
048    import org.eclipse.jetty.server.Server;
049    import org.eclipse.jetty.server.handler.ContextHandlerCollection;
050    import org.eclipse.jetty.server.handler.HandlerCollection;
051    import org.eclipse.jetty.server.handler.HandlerWrapper;
052    import org.eclipse.jetty.server.nio.SelectChannelConnector;
053    import org.eclipse.jetty.server.session.SessionHandler;
054    import org.eclipse.jetty.server.ssl.SslSocketConnector;
055    import org.eclipse.jetty.servlet.FilterHolder;
056    import org.eclipse.jetty.servlet.ServletContextHandler;
057    import org.eclipse.jetty.servlet.ServletHolder;
058    import org.eclipse.jetty.servlets.MultiPartFilter;
059    import org.eclipse.jetty.util.component.LifeCycle;
060    import org.eclipse.jetty.util.thread.QueuedThreadPool;
061    import org.eclipse.jetty.util.thread.ThreadPool;
062    
063    /**
064     * An HttpComponent which starts an embedded Jetty for to handle consuming from
065     * the http endpoints.
066     *
067     * @version $Revision: 957588 $
068     */
069    public class JettyHttpComponent extends HttpComponent {
070        public static final String TMP_DIR = "CamelJettyTempDir";
071        
072        protected static final HashMap<String, ConnectorRef> CONNECTORS = new HashMap<String, ConnectorRef>();
073       
074        private static final transient Log LOG = LogFactory.getLog(JettyHttpComponent.class);
075        private static final String JETTY_SSL_KEYSTORE = "org.eclipse.jetty.ssl.keystore";
076        
077        protected String sslKeyPassword;
078        protected String sslPassword;
079        protected String sslKeystore;
080        protected Map<Integer, SslSocketConnector> sslSocketConnectors;
081        protected HttpClient httpClient;
082        protected ThreadPool httpClientThreadPool;
083        protected Integer httpClientMinThreads;
084        protected Integer httpClientMaxThreads;
085        protected MBeanContainer mbContainer;
086        protected boolean enableJmx;
087    
088        class ConnectorRef {
089            Server server;
090            Connector connector;
091            CamelServlet servlet;
092            int refCount;
093    
094            public ConnectorRef(Server server, Connector connector, CamelServlet servlet) {
095                this.server = server;
096                this.connector = connector;
097                this.servlet = servlet;
098                increment();
099            }
100    
101            public int increment() {
102                return ++refCount;
103            }
104    
105            public int decrement() {
106                return --refCount;
107            }
108        }
109    
110        @Override
111        protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
112            uri = uri.startsWith("jetty:") ? remaining : uri;
113    
114            // must extract well known parameters before we create the endpoint
115            List<Handler> handlerList = resolveAndRemoveReferenceListParameter(parameters, "handlers", Handler.class);
116            HttpBinding binding = resolveAndRemoveReferenceParameter(parameters, "httpBindingRef", HttpBinding.class);
117            Boolean throwExceptionOnFailure = getAndRemoveParameter(parameters, "throwExceptionOnFailure", Boolean.class);
118            Boolean bridgeEndpoint = getAndRemoveParameter(parameters, "bridgeEndpoint", Boolean.class);
119            Boolean matchOnUriPrefix = getAndRemoveParameter(parameters, "matchOnUriPrefix", Boolean.class);
120            Boolean enableJmx = getAndRemoveParameter(parameters, "enableJmx", Boolean.class);
121    
122            // configure http client if we have url configuration for it
123            // http client is only used for jetty http producer (hence not very commonly used)
124            HttpClient client = null;
125            if (IntrospectionSupport.hasProperties(parameters, "httpClient.")) {
126                // set additional parameters on http client
127                // only create client when needed
128                client = getHttpClient();
129                IntrospectionSupport.setProperties(client, parameters, "httpClient.");
130                // validate that we could resolve all httpClient. parameters as this component is lenient
131                validateParameters(uri, parameters, "httpClient.");
132            }
133    
134            // restructure uri to be based on the parameters left as we dont want to include the Camel internal options
135            URI httpUri = URISupport.createRemainingURI(new URI(uri), CastUtils.cast(parameters));
136            uri = httpUri.toString();
137    
138            // create endpoint after all known parameters have been extracted from parameters
139            JettyHttpEndpoint endpoint = new JettyHttpEndpoint(this, uri, httpUri);
140            setEndpointHeaderFilterStrategy(endpoint);
141    
142            if (client != null) {
143                endpoint.setClient(client);
144            }
145            if (handlerList.size() > 0) {
146                endpoint.setHandlers(handlerList);
147            }
148            // prefer to use endpoint configured over component configured
149            if (binding == null) {
150                // fallback to component configured
151                binding = getHttpBinding();
152            }
153            if (binding != null) {
154                endpoint.setBinding(binding);
155            }
156            // should we use an exception for failed error codes?
157            if (throwExceptionOnFailure != null) {
158                endpoint.setThrowExceptionOnFailure(throwExceptionOnFailure);
159            }
160            if (bridgeEndpoint != null) {
161                endpoint.setBridgeEndpoint(bridgeEndpoint);
162            }
163            if (matchOnUriPrefix != null) {
164                endpoint.setMatchOnUriPrefix(matchOnUriPrefix);
165            }
166            
167            if (enableJmx != null) {
168                endpoint.setEnableJmx(enableJmx);
169            } else { 
170                // set this option based on setting of JettyHttpComponent
171                endpoint.setEnableJmx(isEnableJmx());
172            }
173    
174            setProperties(endpoint, parameters);
175            return endpoint;
176        }
177    
178        /**
179         * Connects the URL specified on the endpoint to the specified processor.
180         */
181        @Override
182        public void connect(HttpConsumer consumer) throws Exception {
183            // Make sure that there is a connector for the requested endpoint.
184            JettyHttpEndpoint endpoint = (JettyHttpEndpoint)consumer.getEndpoint();
185            String connectorKey = getConnectorKey(endpoint);
186    
187            synchronized (CONNECTORS) {
188                ConnectorRef connectorRef = CONNECTORS.get(connectorKey);
189                if (connectorRef == null) {
190                    Connector connector;
191                    if ("https".equals(endpoint.getProtocol())) {
192                        connector = getSslSocketConnector(endpoint.getPort());
193                    } else {
194                        connector = new SelectChannelConnector();
195                    }
196                    connector.setPort(endpoint.getPort());
197                    connector.setHost(endpoint.getHttpUri().getHost());
198                    if ("localhost".equalsIgnoreCase(endpoint.getHttpUri().getHost())) {
199                        LOG.warn("You use localhost interface! It means that no external connections will be available."
200                                + " Don't you want to use 0.0.0.0 instead (all network interfaces)? " + endpoint);
201                    }
202                    Server server = createServer();
203                    if (endpoint.isEnableJmx()) {
204                        enableJmx(server);
205                    }
206                    server.addConnector(connector);
207    
208                    connectorRef = new ConnectorRef(server, connector, createServletForConnector(server, connector, endpoint.getHandlers()));
209                    // must enable session before we start
210                    if (endpoint.isSessionSupport()) {
211                        enableSessionSupport(connectorRef.server, connectorKey);
212                    }
213                    connectorRef.server.start();
214                    
215                    CONNECTORS.put(connectorKey, connectorRef);
216                    
217                } else {
218                    // ref track the connector
219                    connectorRef.increment();
220                }
221                // check the session support
222                if (endpoint.isSessionSupport()) {
223                    enableSessionSupport(connectorRef.server, connectorKey);
224                }
225                connectorRef.servlet.connect(consumer);
226            }
227        }
228        
229        private void enableJmx(Server server) {
230            MBeanContainer containerToRegister = getMbContainer();
231            if (containerToRegister != null) {
232                LOG.info("Jetty JMX Extensions is enabled");
233                server.getContainer().addEventListener(containerToRegister);
234                // Since we may have many Servers running, don't tie the MBeanContainer
235                // to a Server lifecycle or we end up closing it while it is still in use.
236                //server.addBean(mbContainer);
237            }
238        }
239    
240        private void enableSessionSupport(Server server, String connectorKey) throws Exception {
241            ServletContextHandler context = (ServletContextHandler)server.getChildHandlerByClass(ServletContextHandler.class);
242            if (context.getSessionHandler() == null) {
243                SessionHandler sessionHandler = new SessionHandler();
244                if (context.isStarted()) {
245                    throw new IllegalStateException("Server has already been started. Cannot enabled sessionSupport on " + connectorKey);
246                } else {
247                    context.setSessionHandler(sessionHandler);
248                }
249            }
250        }
251    
252        /**
253         * Disconnects the URL specified on the endpoint from the specified processor.
254         */
255        @Override
256        public void disconnect(HttpConsumer consumer) throws Exception {
257            // If the connector is not needed anymore then stop it
258            HttpEndpoint endpoint = consumer.getEndpoint();
259            String connectorKey = getConnectorKey(endpoint);
260            
261            synchronized (CONNECTORS) {
262                ConnectorRef connectorRef = CONNECTORS.get(connectorKey);
263                if (connectorRef != null) {
264                    connectorRef.servlet.disconnect(consumer);
265                    if (connectorRef.decrement() == 0) {
266                        connectorRef.server.removeConnector(connectorRef.connector);
267                        connectorRef.connector.stop();
268                        connectorRef.server.stop();
269                        CONNECTORS.remove(connectorKey);
270                        // Camel controls the lifecycle of these entities so remove the
271                        // registered MBeans when Camel is done with the managed objects.
272                        if (mbContainer != null) {
273                            mbContainer.removeBean(connectorRef.server);
274                            mbContainer.removeBean(connectorRef.connector);
275                        }
276                    }
277                }
278            }
279        }
280        
281        private String getConnectorKey(HttpEndpoint endpoint) {
282            return endpoint.getProtocol() + ":" + endpoint.getHttpUri().getHost() + ":" + endpoint.getPort();
283        }
284    
285        // Properties
286        // -------------------------------------------------------------------------
287           
288        public String getSslKeyPassword() {
289            return sslKeyPassword;
290        }
291    
292        public void setSslKeyPassword(String sslKeyPassword) {
293            this.sslKeyPassword = sslKeyPassword;
294        }
295    
296        public String getSslPassword() {
297            return sslPassword;
298        }
299    
300        public void setSslPassword(String sslPassword) {
301            this.sslPassword = sslPassword;
302        }
303    
304        public void setKeystore(String sslKeystore) {
305            this.sslKeystore = sslKeystore;
306        }
307    
308        public String getKeystore() {
309            return sslKeystore;
310        }
311    
312        public SslSocketConnector getSslSocketConnector(int port) {
313            SslSocketConnector answer = null;
314            if (sslSocketConnectors != null) {
315                answer = sslSocketConnectors.get(port);
316            }
317            if (answer == null) {
318                answer = createSslSocketConnector();
319            } else {
320                // try the keystore system property as a backup, jetty doesn't seem
321                // to read this property anymore
322                String keystoreProperty = System.getProperty(JETTY_SSL_KEYSTORE);
323                if (keystoreProperty != null) {
324                    answer.setKeystore(keystoreProperty);
325                }
326    
327            }
328            return answer;
329        }
330        
331        public SslSocketConnector createSslSocketConnector() {
332            SslSocketConnector answer = new SslSocketConnector();
333            // with default null values, jetty ssl system properties
334            // and console will be read by jetty implementation
335            answer.setPassword(sslPassword);
336            answer.setKeyPassword(sslKeyPassword);
337            if (sslKeystore != null) {
338                answer.setKeystore(sslKeystore);
339            } else {
340                // try the keystore system property as a backup, jetty doesn't seem
341                // to read this property anymore
342                String keystoreProperty = System.getProperty(JETTY_SSL_KEYSTORE);
343                if (keystoreProperty != null) {
344                    answer.setKeystore(keystoreProperty);
345                }
346            }
347            
348            return answer;
349        }
350    
351        public void setSslSocketConnectors(Map <Integer, SslSocketConnector> connectors) {
352            sslSocketConnectors = connectors;
353        }
354    
355        public synchronized HttpClient getHttpClient() {
356            if (httpClient == null) {
357                httpClient = new HttpClient();
358                httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
359    
360                if (System.getProperty("http.proxyHost") != null && System.getProperty("http.proxyPort") != null) {
361                    String host = System.getProperty("http.proxyHost");
362                    int port = Integer.parseInt(System.getProperty("http.proxyPort"));
363                    if (LOG.isDebugEnabled()) {
364                        LOG.debug("Java System Property http.proxyHost and http.proxyPort detected. Using http proxy host: "
365                                + host + " port: " + port);
366                    }
367                    httpClient.setProxy(new Address(host, port));
368                }
369    
370                // use QueueThreadPool as the default bounded is deprecated (see SMXCOMP-157)
371                if (getHttpClientThreadPool() == null) {
372                    QueuedThreadPool qtp = new QueuedThreadPool();
373                    if (httpClientMinThreads != null) {
374                        qtp.setMinThreads(httpClientMinThreads.intValue());
375                    }
376                    if (httpClientMaxThreads != null) {
377                        qtp.setMaxThreads(httpClientMaxThreads.intValue());
378                    }
379                    try {
380                        qtp.start();
381                    } catch (Exception e) {
382                        throw new RuntimeCamelException("Error starting JettyHttpClient thread pool: " + qtp, e);
383                    }
384                    setHttpClientThreadPool(qtp);
385                }
386                httpClient.setThreadPool(getHttpClientThreadPool());
387            }
388            return httpClient;
389        }
390    
391        public void setHttpClient(HttpClient httpClient) {
392            this.httpClient = httpClient;
393        }
394    
395        public ThreadPool getHttpClientThreadPool() {
396            return httpClientThreadPool;
397        }
398    
399        public void setHttpClientThreadPool(ThreadPool httpClientThreadPool) {
400            this.httpClientThreadPool = httpClientThreadPool;
401        }
402    
403        public Integer getHttpClientMinThreads() {
404            return httpClientMinThreads;
405        }
406    
407        public void setHttpClientMinThreads(Integer httpClientMinThreads) {
408            this.httpClientMinThreads = httpClientMinThreads;
409        }
410    
411        public Integer getHttpClientMaxThreads() {
412            return httpClientMaxThreads;
413        }
414    
415        public void setHttpClientMaxThreads(Integer httpClientMaxThreads) {
416            this.httpClientMaxThreads = httpClientMaxThreads;
417        }
418    
419        public void setEnableJmx(boolean enableJmx) {
420            this.enableJmx = enableJmx;
421        }
422    
423        public boolean isEnableJmx() {
424            return enableJmx;
425        }
426    
427        public synchronized MBeanContainer getMbContainer() {
428            // If null, provide the default implementation.
429            if (mbContainer == null) {
430                MBeanServer mbs = null;
431                
432                final ManagementStrategy mStrategy = this.getCamelContext().getManagementStrategy();
433                final ManagementAgent mAgent = mStrategy.getManagementAgent();
434                if (mAgent != null) {
435                    mbs = mAgent.getMBeanServer();
436                }
437                
438                if (mbs != null) {
439                    mbContainer = new MBeanContainer(mbs);
440                    startMbContainer();
441                } else {
442                    LOG.warn("JMX disabled in CamelContext. Jetty JMX extensions will remain disabled.");
443                }
444            }
445            
446            return this.mbContainer;
447        }
448    
449        public void setMbContainer(MBeanContainer mbContainer) {
450            this.mbContainer = mbContainer;
451        }
452    
453        // Implementation methods
454        // -------------------------------------------------------------------------
455        protected CamelServlet createServletForConnector(Server server, Connector connector, List<Handler> handlers) throws Exception {
456            ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
457            context.setConnectorNames(new String[] {connector.getName()});
458    
459            if (handlers != null && !handlers.isEmpty()) {
460                for (Handler handler : handlers) {
461                    if (handler instanceof HandlerWrapper) {
462                        ((HandlerWrapper) handler).setHandler(server.getHandler());
463                        server.setHandler(handler);
464                    } else {
465                        HandlerCollection handlerCollection = new HandlerCollection();
466                        handlerCollection.addHandler(server.getHandler());
467                        handlerCollection.addHandler(handler);
468                        server.setHandler(handlerCollection);
469                    }
470                }
471            }
472    
473            // use Jetty continuations
474            CamelServlet camelServlet = new CamelContinuationServlet();
475            ServletHolder holder = new ServletHolder();
476            holder.setServlet(camelServlet);
477            CamelContext camelContext = this.getCamelContext();
478            FilterHolder filterHolder = new FilterHolder();
479            filterHolder.setInitParameter("deleteFiles", "true");
480            if (ObjectHelper.isNotEmpty(camelContext.getProperties().get(TMP_DIR))) {
481                File file =  new File(camelContext.getProperties().get(TMP_DIR));            
482                if (!file.isDirectory()) {
483                    throw new RuntimeCamelException("The temp file directory of camel-jetty is not exists, please recheck it with directory name :"
484                                                    + camelContext.getProperties().get(TMP_DIR));
485                }
486                context.setAttribute("javax.servlet.context.tempdir", file);
487            }
488            filterHolder.setFilter(new MultiPartFilter());
489            //add the default MultiPartFilter filter for it
490            context.addFilter(filterHolder, "/*", 0);
491            context.addServlet(holder, "/*");
492    
493            return camelServlet;
494        }
495        
496        protected Server createServer() throws Exception {
497            Server server = new Server();
498            ContextHandlerCollection collection = new ContextHandlerCollection();
499            server.setHandler(collection);
500            return server;
501        }
502        
503        /**
504         * Starts {@link #mbContainer} and registers the container with itself as a managed bean
505         * logging an error if there is a problem starting the container.
506         * Does nothing if {@link #mbContainer} is {@code null}.
507         */
508        protected void startMbContainer() {
509            if (mbContainer != null && !mbContainer.isStarted()) {
510                try {
511                    mbContainer.start();
512                    // Publish the container itself for consistency with
513                    // traditional embedded Jetty configurations.
514                    mbContainer.addBean(mbContainer);
515                } catch (Throwable e) {
516                    LOG.warn("Could not start Jetty MBeanContainer. Jetty JMX extensions will remain disabled.", e);
517                }
518            }
519        }
520    
521        @Override
522        protected void doStart() throws Exception {
523            super.doStart();
524            if (httpClientThreadPool != null && httpClientThreadPool instanceof LifeCycle) {
525                LifeCycle lc = (LifeCycle) httpClientThreadPool;
526                lc.start();
527            }
528            if (httpClient != null && !httpClient.isStarted()) {
529                httpClient.start();
530            }
531            
532            startMbContainer();
533        }
534    
535        @Override
536        protected void doStop() throws Exception {
537            super.doStop();
538            if (CONNECTORS.size() > 0) {
539                for (ConnectorRef connectorRef : CONNECTORS.values()) {
540                    connectorRef.server.removeConnector(connectorRef.connector);
541                    connectorRef.connector.stop();
542                    connectorRef.server.stop();
543                    // Camel controls the lifecycle of these entities so remove the
544                    // registered MBeans when Camel is done with the managed objects.
545                    if (mbContainer != null) {
546                        mbContainer.removeBean(connectorRef.server);
547                        mbContainer.removeBean(connectorRef.connector);
548                    }
549                }
550                CONNECTORS.clear();
551            }
552            if (httpClient != null) {
553                httpClient.stop();
554            }
555            if (httpClientThreadPool != null && httpClientThreadPool instanceof LifeCycle) {
556                LifeCycle lc = (LifeCycle) httpClientThreadPool;
557                lc.stop();
558            }
559            if (mbContainer != null) {
560                mbContainer.stop();
561            }
562        }
563    }