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