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