1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18
19 package io.undertow.servlet.handlers;
20
21 import static io.undertow.servlet.handlers.ServletPathMatch.Type.REDIRECT;
22 import static io.undertow.servlet.handlers.ServletPathMatch.Type.REWRITE;
23
24 import java.io.File;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.EnumMap;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
33 import javax.servlet.DispatcherType;
34 import javax.servlet.http.MappingMatch;
35
36 import io.undertow.server.HandlerWrapper;
37 import io.undertow.server.HttpHandler;
38 import io.undertow.server.handlers.cache.LRUCache;
39 import io.undertow.server.handlers.resource.Resource;
40 import io.undertow.server.handlers.resource.ResourceManager;
41 import io.undertow.servlet.UndertowServletMessages;
42 import io.undertow.servlet.api.Deployment;
43 import io.undertow.servlet.api.DeploymentInfo;
44 import io.undertow.servlet.api.FilterMappingInfo;
45 import io.undertow.servlet.api.ServletInfo;
46 import io.undertow.servlet.core.ManagedFilter;
47 import io.undertow.servlet.core.ManagedFilters;
48 import io.undertow.servlet.core.ManagedServlet;
49 import io.undertow.servlet.core.ManagedServlets;
50 import io.undertow.servlet.handlers.security.ServletSecurityRoleHandler;
51
52 /**
53  * Facade around {@link ServletPathMatchesData}. This facade is responsible for re-generating the matches if anything changes.
54  *
55  * @author Stuart Douglas
56  */

57 public class ServletPathMatches {
58
59     public static final String DEFAULT_SERVLET_NAME = "default";
60     private final Deployment deployment;
61
62     private volatile String[] welcomePages;
63     private final ResourceManager resourceManager;
64
65     private volatile ServletPathMatchesData data;
66
67     private final LRUCache<String, ServletPathMatch> pathMatchCache = new LRUCache<>(1000, -1, true); //TODO: configurable
68
69     public ServletPathMatches(final Deployment deployment) {
70         this.deployment = deployment;
71         this.welcomePages = deployment.getDeploymentInfo().getWelcomePages().toArray(new String[deployment.getDeploymentInfo().getWelcomePages().size()]);
72         this.resourceManager = deployment.getDeploymentInfo().getResourceManager();
73     }
74
75     public void initData(){
76         getData();
77     }
78
79     public ServletChain getServletHandlerByName(final String name) {
80         return getData().getServletHandlerByName(name);
81     }
82
83     public ServletPathMatch getServletHandlerByPath(final String path) {
84         ServletPathMatch existing = pathMatchCache.get(path);
85         if(existing != null) {
86             return existing;
87         }
88         ServletPathMatch match = getData().getServletHandlerByPath(path);
89         if (!match.isRequiredWelcomeFileMatch()) {
90             pathMatchCache.add(path, match);
91             return match;
92         }
93         try {
94
95             String remaining = match.getRemaining() == null ? match.getMatched() : match.getRemaining();
96             Resource resource = resourceManager.getResource(remaining);
97             if (resource == null || !resource.isDirectory()) {
98                 pathMatchCache.add(path, match);
99                 return match;
100             }
101
102             boolean pathEndsWithSlash = remaining.endsWith("/");
103             final String pathWithTrailingSlash = pathEndsWithSlash ? remaining : remaining + "/";
104
105             ServletPathMatch welcomePage = findWelcomeFile(pathWithTrailingSlash, !pathEndsWithSlash);
106
107             if (welcomePage != null) {
108                 pathMatchCache.add(path, welcomePage);
109                 return welcomePage;
110             } else {
111                 welcomePage = findWelcomeServlet(pathWithTrailingSlash, !pathEndsWithSlash);
112                 if (welcomePage != null) {
113                     pathMatchCache.add(path, welcomePage);
114                     return welcomePage;
115                 } else if(pathEndsWithSlash) {
116                     pathMatchCache.add(path, match);
117                     return match;
118                 } else {
119                     ServletPathMatch redirect = new ServletPathMatch(match.getServletChain(), match.getMatched(), match.getRemaining(), REDIRECT, "/");
120                     pathMatchCache.add(path, redirect);
121                     return redirect;
122                 }
123             }
124
125         } catch (IOException e) {
126             throw new RuntimeException(e);
127         }
128
129     }
130
131     public void invalidate() {
132         this.data = null;
133         this.pathMatchCache.clear();
134     }
135
136     private ServletPathMatchesData getData() {
137         ServletPathMatchesData data = this.data;
138         if (data != null) {
139             return data;
140         }
141         synchronized (this) {
142             if (this.data != null) {
143                 return this.data;
144             }
145             return this.data = setupServletChains();
146         }
147     }
148
149     private ServletPathMatch findWelcomeFile(final String path, boolean requiresRedirect) {
150         if(File.separatorChar != '/' && path.contains(File.separator)) {
151             return null;
152         }
153         StringBuilder sb = new StringBuilder();
154         for (String i : welcomePages) {
155             try {
156                 sb.append(path);
157                 sb.append(i);
158                 final String mergedPath = sb.toString();
159                 sb.setLength(0);
160                 Resource resource = resourceManager.getResource(mergedPath);
161                 if (resource != null) {
162                     final ServletPathMatch handler = data.getServletHandlerByPath(mergedPath);
163                     return new ServletPathMatch(handler.getServletChain(), mergedPath, null, requiresRedirect ? REDIRECT : REWRITE, mergedPath);
164                 }
165             } catch (IOException e) {
166             }
167         }
168         return null;
169     }
170
171     private ServletPathMatch findWelcomeServlet(final String path, boolean requiresRedirect) {
172         StringBuilder sb = new StringBuilder();
173         for (String i : welcomePages) {
174             sb.append(path);
175             sb.append(i);
176             final String mergedPath = sb.toString();
177             sb.setLength(0);
178             final ServletPathMatch handler = data.getServletHandlerByPath(mergedPath);
179             if (handler != null && !handler.isRequiredWelcomeFileMatch()) {
180                 return new ServletPathMatch(handler.getServletChain(), handler.getMatched(), handler.getRemaining(), requiresRedirect ? REDIRECT : REWRITE, mergedPath);
181             }
182         }
183         return null;
184     }
185
186     public void setWelcomePages(List<String> welcomePages) {
187         this.welcomePages = welcomePages.toArray(new String[welcomePages.size()]);
188     }
189
190     /**
191      * Sets up the handlers in the servlet chain. We setup a chain for every path + extension match possibility.
192      * (i.e. if there a m path mappings and n extension mappings we have n*m chains).
193      * <p>
194      * If a chain consists of only the default servlet then we add it as an async handler, so that resources can be
195      * served up directly without using blocking operations.
196      * <p>
197      * TODO: this logic is a bit convoluted at the moment, we should look at simplifying it
198      */

199     private ServletPathMatchesData setupServletChains() {
200         //create the default servlet
201         ServletHandler defaultServlet = null;
202         final ManagedServlets servlets = deployment.getServlets();
203         final ManagedFilters filters = deployment.getFilters();
204
205         final Map<String, ServletHandler> extensionServlets = new HashMap<>();
206         final Map<String, ServletHandler> pathServlets = new HashMap<>();
207
208         final Set<String> pathMatches = new HashSet<>();
209         final Set<String> extensionMatches = new HashSet<>();
210
211         DeploymentInfo deploymentInfo = deployment.getDeploymentInfo();
212
213         //loop through all filter mappings, and add them to the set of known paths
214         for (FilterMappingInfo mapping : deploymentInfo.getFilterMappings()) {
215             if (mapping.getMappingType() == FilterMappingInfo.MappingType.URL) {
216                 String path = mapping.getMapping();
217                 if (path.equals("*")) {
218                     //UNDERTOW-95, support this non-standard filter mapping
219                     path = "/*";
220                 }
221                 if (!path.startsWith("*.")) {
222                     pathMatches.add(path);
223                 } else {
224                     extensionMatches.add(path.substring(2));
225                 }
226             }
227         }
228
229         //now loop through all servlets.
230         for (Map.Entry<String, ServletHandler> entry : servlets.getServletHandlers().entrySet()) {
231             final ServletHandler handler = entry.getValue();
232             //add the servlet to the approprite path maps
233             for (String path : handler.getManagedServlet().getServletInfo().getMappings()) {
234                 if (path.equals("/")) {
235                     //the default servlet
236                     pathMatches.add("/*");
237                     if (defaultServlet != null) {
238                         throw UndertowServletMessages.MESSAGES.twoServletsWithSameMapping(path);
239                     }
240                     defaultServlet = handler;
241                 } else if (!path.startsWith("*.")) {
242                     //either an exact or a /* based path match
243                     if (path.isEmpty()) {
244                         path = "/";
245                     }
246                     pathMatches.add(path);
247                     if (pathServlets.containsKey(path)) {
248                         throw UndertowServletMessages.MESSAGES.twoServletsWithSameMapping(path);
249                     }
250                     pathServlets.put(path, handler);
251                 } else {
252                     //an extension match based servlet
253                     String ext = path.substring(2);
254                     extensionMatches.add(ext);
255                     if(extensionServlets.containsKey(ext)) {
256                         throw UndertowServletMessages.MESSAGES.twoServletsWithSameMapping(path);
257                     }
258                     extensionServlets.put(ext, handler);
259                 }
260             }
261         }
262         ServletHandler managedDefaultServlet = servlets.getServletHandler(DEFAULT_SERVLET_NAME);
263         if(managedDefaultServlet == null) {
264             //we always create a default servlet, even if it is not going to have any path mappings registered
265             managedDefaultServlet = servlets.addServlet(new ServletInfo(DEFAULT_SERVLET_NAME, DefaultServlet.class));
266         }
267
268         if (defaultServlet == null) {
269             //no explicit default servlet was specified, so we register our mapping
270             pathMatches.add("/*");
271             defaultServlet = managedDefaultServlet;
272         }
273
274         final ServletPathMatchesData.Builder builder = ServletPathMatchesData.builder();
275
276         //we now loop over every path in the application, and build up the patches based on this path
277         //these paths contain both /* and exact matches.
278         for (final String path : pathMatches) {
279             //resolve the target servlet, will return null if this is the default servlet
280             MatchData targetServletMatch = resolveServletForPath(path, pathServlets, extensionServlets, defaultServlet);
281
282             final Map<DispatcherType, List<ManagedFilter>> noExtension = new EnumMap<>(DispatcherType.class);
283             final Map<String, Map<DispatcherType, List<ManagedFilter>>> extension = new HashMap<>();
284             //initalize the extension map. This contains all the filers in the noExtension map, plus
285             //any filters that match the extension key
286             for (String ext : extensionMatches) {
287                 extension.put(ext, new EnumMap<DispatcherType, List<ManagedFilter>>(DispatcherType.class));
288             }
289
290             //loop over all the filters, and add them to the appropriate map in the correct order
291             for (final FilterMappingInfo filterMapping : deploymentInfo.getFilterMappings()) {
292                 ManagedFilter filter = filters.getManagedFilter(filterMapping.getFilterName());
293                 if (filterMapping.getMappingType() == FilterMappingInfo.MappingType.SERVLET) {
294                     if (targetServletMatch.handler != null) {
295                         if (filterMapping.getMapping().equals(targetServletMatch.handler.getManagedServlet().getServletInfo().getName()) || filterMapping.getMapping().equals("*")) {
296                             addToListMap(noExtension, filterMapping.getDispatcher(), filter);
297                         }
298                     }
299                     for (Map.Entry<String, Map<DispatcherType, List<ManagedFilter>>> entry : extension.entrySet()) {
300                         ServletHandler pathServlet = targetServletMatch.handler;
301                         boolean defaultServletMatch = targetServletMatch.defaultServlet;
302                         if (defaultServletMatch && extensionServlets.containsKey(entry.getKey())) {
303                             pathServlet = extensionServlets.get(entry.getKey());
304                         }
305
306                         if (filterMapping.getMapping().equals(pathServlet.getManagedServlet().getServletInfo().getName()) || filterMapping.getMapping().equals("*")) {
307                             addToListMap(extension.get(entry.getKey()), filterMapping.getDispatcher(), filter);
308                         }
309                     }
310                 } else {
311                     if (filterMapping.getMapping().isEmpty() || !filterMapping.getMapping().startsWith("*.")) {
312                         if (isFilterApplicable(path, filterMapping.getMapping())) {
313                             addToListMap(noExtension, filterMapping.getDispatcher(), filter);
314                             for (Map<DispatcherType, List<ManagedFilter>> l : extension.values()) {
315                                 addToListMap(l, filterMapping.getDispatcher(), filter);
316                             }
317                         }
318                     } else {
319                         addToListMap(extension.get(filterMapping.getMapping().substring(2)), filterMapping.getDispatcher(), filter);
320                     }
321                 }
322             }
323             //resolve any matches and add them to the builder
324             if (path.endsWith("/*")) {
325                 String prefix = path.substring(0, path.length() - 2);
326                 //add the default non-extension match
327                 builder.addPrefixMatch(prefix, createHandler(deploymentInfo, targetServletMatch.handler, noExtension, targetServletMatch.matchedPath, targetServletMatch.defaultServlet, targetServletMatch.mappingMatch, targetServletMatch.userPath), targetServletMatch.defaultServlet || targetServletMatch.handler.getManagedServlet().getServletInfo().isRequireWelcomeFileMapping());
328
329                 //build up the chain for each non-extension match
330                 for (Map.Entry<String, Map<DispatcherType, List<ManagedFilter>>> entry : extension.entrySet()) {
331                     ServletHandler pathServlet = targetServletMatch.handler;
332                     String pathMatch = targetServletMatch.matchedPath;
333
334                     boolean defaultServletMatch = targetServletMatch.defaultServlet;
335                     if (defaultServletMatch && extensionServlets.containsKey(entry.getKey())) {
336                         defaultServletMatch = false;
337                         pathServlet = extensionServlets.get(entry.getKey());
338                     }
339                     HttpHandler handler = pathServlet;
340                     if (!entry.getValue().isEmpty()) {
341                         handler = new FilterHandler(entry.getValue(), deploymentInfo.isAllowNonStandardWrappers(), handler);
342                     }
343                     builder.addExtensionMatch(prefix, entry.getKey(), servletChain(handler, pathServlet.getManagedServlet(), entry.getValue(), pathMatch, deploymentInfo, defaultServletMatch, defaultServletMatch ? MappingMatch.DEFAULT : MappingMatch.EXTENSION, defaultServletMatch ? "/" : "*." + entry.getKey()));
344                 }
345             } else if (path.isEmpty()) {
346                 //the context root match
347                 builder.addExactMatch("/", createHandler(deploymentInfo, targetServletMatch.handler, noExtension, targetServletMatch.matchedPath, targetServletMatch.defaultServlet, targetServletMatch.mappingMatch, targetServletMatch.userPath));
348             } else {
349                 //we need to check for an extension match, so paths like /exact.txt will have the correct filter applied
350                 int lastSegmentIndex = path.lastIndexOf('/');
351                 String lastSegment;
352                 if(lastSegmentIndex > 0) {
353                     lastSegment = path.substring(lastSegmentIndex);
354                 } else {
355                     lastSegment = path;
356                 }
357                 if (lastSegment.contains(".")) {
358                     String ext = lastSegment.substring(lastSegment.lastIndexOf('.') + 1);
359                     if (extension.containsKey(ext)) {
360                         Map<DispatcherType, List<ManagedFilter>> extMap = extension.get(ext);
361                         builder.addExactMatch(path, createHandler(deploymentInfo, targetServletMatch.handler, extMap, targetServletMatch.matchedPath, targetServletMatch.defaultServlet, targetServletMatch.mappingMatch, targetServletMatch.userPath));
362                     } else {
363                         builder.addExactMatch(path, createHandler(deploymentInfo, targetServletMatch.handler, noExtension, targetServletMatch.matchedPath, targetServletMatch.defaultServlet, targetServletMatch.mappingMatch, targetServletMatch.userPath));
364                     }
365                 } else {
366                     builder.addExactMatch(path, createHandler(deploymentInfo, targetServletMatch.handler, noExtension, targetServletMatch.matchedPath, targetServletMatch.defaultServlet, targetServletMatch.mappingMatch, targetServletMatch.userPath));
367                 }
368
369             }
370         }
371
372         //now setup name based mappings
373         //these are used for name based dispatch
374         for (Map.Entry<String, ServletHandler> entry : servlets.getServletHandlers().entrySet()) {
375             final Map<DispatcherType, List<ManagedFilter>> filtersByDispatcher = new EnumMap<>(DispatcherType.class);
376             for (final FilterMappingInfo filterMapping : deploymentInfo.getFilterMappings()) {
377                 ManagedFilter filter = filters.getManagedFilter(filterMapping.getFilterName());
378                 if (filterMapping.getMappingType() == FilterMappingInfo.MappingType.SERVLET) {
379                     if (filterMapping.getMapping().equals(entry.getKey())) {
380                         addToListMap(filtersByDispatcher, filterMapping.getDispatcher(), filter);
381                     }
382                 }
383             }
384             if (filtersByDispatcher.isEmpty()) {
385                 builder.addNameMatch(entry.getKey(), servletChain(entry.getValue(), entry.getValue().getManagedServlet(), filtersByDispatcher, null, deploymentInfo, false, MappingMatch.EXACT, ""));
386             } else {
387                 builder.addNameMatch(entry.getKey(), servletChain(new FilterHandler(filtersByDispatcher, deploymentInfo.isAllowNonStandardWrappers(), entry.getValue()), entry.getValue().getManagedServlet(), filtersByDispatcher, null, deploymentInfo, false, MappingMatch.EXACT, ""));
388             }
389         }
390
391         return builder.build();
392     }
393
394     private ServletChain createHandler(final DeploymentInfo deploymentInfo, final ServletHandler targetServlet, final Map<DispatcherType, List<ManagedFilter>> noExtension, final String servletPath, final boolean defaultServlet, MappingMatch mappingMatch, String pattern) {
395         final ServletChain initialHandler;
396         if (noExtension.isEmpty()) {
397             initialHandler = servletChain(targetServlet, targetServlet.getManagedServlet(), noExtension, servletPath, deploymentInfo, defaultServlet, mappingMatch, pattern);
398         } else {
399             FilterHandler handler = new FilterHandler(noExtension, deploymentInfo.isAllowNonStandardWrappers(), targetServlet);
400             initialHandler = servletChain(handler, targetServlet.getManagedServlet(), noExtension, servletPath, deploymentInfo, defaultServlet, mappingMatch, pattern);
401         }
402         return initialHandler;
403     }
404
405     private static MatchData resolveServletForPath(final String path, final Map<String, ServletHandler> pathServlets, final Map<String, ServletHandler> extensionServlets, ServletHandler defaultServlet) {
406         if (pathServlets.containsKey(path)) {
407             if (path.endsWith("/*")) {
408                 final String base = path.substring(0, path.length() - 2);
409                 return new MatchData(pathServlets.get(path), base, path, MappingMatch.PATH, false);
410             } else {
411                 if(path.equals("/")) {
412                     return new MatchData(pathServlets.get(path), path, "", MappingMatch.CONTEXT_ROOT, false);
413                 }
414                 return new MatchData(pathServlets.get(path), path, path, MappingMatch.EXACT, false);
415             }
416         }
417         String match = null;
418         ServletHandler servlet = null;
419         String userPath = "";
420         for (final Map.Entry<String, ServletHandler> entry : pathServlets.entrySet()) {
421             String key = entry.getKey();
422             if (key.endsWith("/*")) {
423                 final String base = key.substring(0, key.length() - 1);
424                 if (match == null || base.length() > match.length()) {
425                     if (path.startsWith(base) || path.equals(base.substring(0, base.length() - 1))) {
426                         match = base.substring(0, base.length() - 1);
427                         servlet = entry.getValue();
428                         userPath = key;
429                     }
430                 }
431             }
432         }
433         if (servlet != null) {
434             return new MatchData(servlet, match, userPath, MappingMatch.PATH, false);
435         }
436         int index = path.lastIndexOf('.');
437         if (index != -1) {
438             String ext = path.substring(index + 1);
439             servlet = extensionServlets.get(ext);
440             if (servlet != null) {
441                 return new MatchData(servlet, null"*." + ext, MappingMatch.EXTENSION, false);
442             }
443         }
444
445         return new MatchData(defaultServlet, null"/", MappingMatch.DEFAULT, true);
446     }
447
448     private static boolean isFilterApplicable(final String path, final String filterPath) {
449         String modifiedPath;
450         if (filterPath.equals("*")) {
451             modifiedPath = "/*";
452         } else {
453             modifiedPath = filterPath;
454         }
455         if (path.isEmpty()) {
456             return modifiedPath.equals("/*") || modifiedPath.equals("/");
457         }
458         if (modifiedPath.endsWith("/*")) {
459             String baseFilterPath = modifiedPath.substring(0, modifiedPath.length() - 1);
460             String exactFilterPath = modifiedPath.substring(0, modifiedPath.length() - 2);
461             return path.startsWith(baseFilterPath) || path.equals(exactFilterPath);
462         } else {
463             return modifiedPath.equals(path);
464         }
465     }
466
467     private static <K, V> void addToListMap(final Map<K, List<V>> map, final K key, final V value) {
468         List<V> list = map.get(key);
469         if (list == null) {
470             map.put(key, list = new ArrayList<>());
471         }
472         list.add(value);
473     }
474
475     private static ServletChain servletChain(HttpHandler next, final ManagedServlet managedServlet, Map<DispatcherType, List<ManagedFilter>> filters, final String servletPath, final DeploymentInfo deploymentInfo, boolean defaultServlet, MappingMatch mappingMatch, String pattern) {
476         HttpHandler servletHandler = next;
477         if(!deploymentInfo.isSecurityDisabled()) {
478             servletHandler = new ServletSecurityRoleHandler(servletHandler, deploymentInfo.getAuthorizationManager());
479         }
480         servletHandler = wrapHandlers(servletHandler, managedServlet.getServletInfo().getHandlerChainWrappers());
481         return new ServletChain(servletHandler, managedServlet, servletPath, defaultServlet, mappingMatch, pattern, filters);
482     }
483
484     private static HttpHandler wrapHandlers(final HttpHandler wrapee, final List<HandlerWrapper> wrappers) {
485         HttpHandler current = wrapee;
486         for (HandlerWrapper wrapper : wrappers) {
487             current = wrapper.wrap(current);
488         }
489         return current;
490     }
491
492     private static class MatchData {
493         final ServletHandler handler;
494         final String matchedPath;
495         final String userPath;
496         final MappingMatch mappingMatch;
497         final boolean defaultServlet;
498
499         private MatchData(final ServletHandler handler, final String matchedPath, String userPath, MappingMatch mappingMatch, boolean defaultServlet) {
500             this.handler = handler;
501             this.matchedPath = matchedPath;
502             this.userPath = userPath;
503             this.mappingMatch = mappingMatch;
504             this.defaultServlet = defaultServlet;
505         }
506     }
507 }
508