001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.markup.html;
018
019import java.util.ArrayList;
020import java.util.List;
021import java.util.concurrent.ConcurrentHashMap;
022import java.util.concurrent.ConcurrentLinkedQueue;
023import java.util.concurrent.ConcurrentMap;
024import java.util.regex.Pattern;
025
026import org.apache.wicket.util.collections.ReverseListIterator;
027import org.apache.wicket.util.string.Strings;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031
032/**
033 * This is a resource guard which by default denies access to all resources and thus is more secure.
034 * <p/>
035 * All pattern are executed in the order they were provided. All pattern are executed to determine
036 * if access can be granted or not.
037 * <p/>
038 * Note that access to the config data such as get/setPattern() and acceptXXX() is not synchronized.
039 * It is assumed that configuration has finished before the first request gets executed.
040 * <p/>
041 * The rules are fairly simple. Each pattern must start with either "+" (include) or "-" (exclude).
042 * "*" is a placeholder for zero, one or more characters within a file or directory name. "**" is a
043 * placeholder for zero, one or more sub-directories.
044 * <p/>
045 * Examples:
046 * <table border="0">
047 * <caption>Examples</caption>
048 * <tr>
049 * <td>+*.gif</td>
050 * <td>All gif files in all directories</td>
051 * </tr>
052 * <tr>
053 * <td>+test*.*</td>
054 * <td>All files in all directories starting with "test"</td>
055 * </tr>
056 * <tr>
057 * <td>+mydir&#47;*&#47;*.gif</td>
058 * <td>All gif files two levels below the mydir directory. E.g. mydir&#47;dir2&#47;test.gif</td>
059 * </tr>
060 * <tr>
061 * <td>+mydir&#47;**&#47;*.gif</td>
062 * <td>All gif files in all directories below mydir. E.g. mydir&#47;test.gif or
063 * mydir&#47;dir2&#47;dir3&#47;test.gif</td>
064 * </tr>
065 * </table>
066 * 
067 * @see IPackageResourceGuard
068 * @see org.apache.wicket.settings.ResourceSettings#getPackageResourceGuard
069 * @see PackageResourceGuard
070 * 
071 * @author Juergen Donnerstag
072 */
073public class SecurePackageResourceGuard extends PackageResourceGuard
074{
075        /** Log. */
076        private static final Logger log = LoggerFactory.getLogger(SecurePackageResourceGuard.class);
077
078        /** The path separator used */
079        private static final char PATH_SEPARATOR = '/';
080
081        /** The list of pattern. Note that the order is important, hence a list */
082        private List<SearchPattern> pattern = new ArrayList<>();
083
084        /** A cache to speed up the checks */
085        private final ConcurrentMap<String, Boolean> cache;
086
087        /**
088         * Constructor.
089         */
090        public SecurePackageResourceGuard()
091        {
092                this(new SimpleCache(100));
093        }
094
095        /**
096         * Constructor.
097         * 
098         * @param cache
099         *            the internal cache that will hold the results for all already checked resources.
100         *            Use {@code null} to disable caching.
101         */
102        public SecurePackageResourceGuard(final ConcurrentMap<String, Boolean> cache)
103        {
104                this.cache = cache;
105
106                // the order is important for better performance
107                // first add the most commonly used
108                addPattern("+*.js");
109                addPattern("+*.css");
110                addPattern("+*.png");
111                addPattern("+*.jpg");
112                addPattern("+*.jpeg");
113                addPattern("+*.gif");
114                addPattern("+*.ico");
115                addPattern("+*.cur");
116                addPattern("+*.map");
117
118                // WICKET-208 non page templates may be served
119                addPattern("+*.html");
120
121                addPattern("+*.txt");
122                addPattern("+*.swf");
123                addPattern("+*.bmp");
124                addPattern("+*.svg");
125                addPattern("+*.avif");
126
127                // allow web fonts
128                addPattern("+*.eot");
129                addPattern("+*.ttf");
130                addPattern("+*.woff");
131                addPattern("+*.woff2");
132
133        }
134
135        /**
136         * 
137         */
138        public void clearCache()
139        {
140                if (cache != null)
141                {
142                        cache.clear();
143                }
144        }
145
146        /**
147         * Whether the provided absolute path is accepted.
148         * 
149         * @param path
150         *            The absolute path, starting from the class root (packages are separated with
151         *            forward slashes instead of dots).
152         * @return True if accepted, false otherwise.
153         */
154        @Override
155        public boolean accept(String path)
156        {
157                // First check the cache
158                if (cache != null)
159                {
160                        Boolean rtn = cache.get(path);
161                        if (rtn != null)
162                        {
163                                return rtn;
164                        }
165                }
166
167                // Check typical files such as log4j.xml etc.
168                if (super.accept(path) == false)
169                {
170                        return false;
171                }
172
173                // Check against the pattern
174                boolean hit = false;
175                for (SearchPattern pattern : new ReverseListIterator<>(this.pattern))
176                {
177                        if ((pattern != null) && pattern.isActive())
178                        {
179                                if (pattern.matches(path))
180                                {
181                                        hit = pattern.isInclude();
182                                        break;
183                                }
184                        }
185                }
186
187                if (cache != null)
188                {
189                        // Do not use putIfAbsent(). See newCache()
190                        cache.put(path, (hit ? Boolean.TRUE : Boolean.FALSE));
191                }
192
193                if (hit == false)
194                {
195                        log.warn("Access denied to shared (static) resource: " + path);
196                }
197
198                return hit;
199        }
200
201        /**
202         * Gets the current list of pattern. Please invoke clearCache() or setPattern(List) when
203         * finished in order to clear the cache of previous checks.
204         * 
205         * @return pattern
206         */
207        public List<SearchPattern> getPattern()
208        {
209                clearCache();
210                return pattern;
211        }
212
213        /**
214         * Sets pattern.
215         * 
216         * @param pattern
217         *            pattern
218         */
219        public void setPattern(List<SearchPattern> pattern)
220        {
221                this.pattern = pattern;
222                clearCache();
223        }
224
225        /**
226         * @param pattern
227         */
228        public void addPattern(String pattern)
229        {
230                this.pattern.add(new SearchPattern(pattern));
231                clearCache();
232        }
233
234        /**
235         * 
236         */
237        public static class SearchPattern
238        {
239                private String pattern;
240
241                private Pattern regex;
242
243                private boolean include;
244
245                private boolean active = true;
246
247                private boolean fileOnly;
248
249                /**
250                 * Construct.
251                 * 
252                 * @param pattern
253                 */
254                public SearchPattern(final String pattern)
255                {
256                        setPattern(pattern);
257                }
258
259                /**
260                 * 
261                 * @param pattern
262                 * @return Regex pattern
263                 */
264                private Pattern convertToRegex(final String pattern)
265                {
266                        String regex = Strings.replaceAll(pattern, ".", "#dot#").toString();
267
268                        // If path starts with "*/" or "**/"
269                        regex = regex.replaceAll("^\\*" + PATH_SEPARATOR, "[^" + PATH_SEPARATOR + "]+" +
270                                PATH_SEPARATOR);
271                        regex = regex.replaceAll("^[\\*]{2,}" + PATH_SEPARATOR, "([^" + PATH_SEPARATOR +
272                                "].#star#" + PATH_SEPARATOR + ")?");
273
274                        // Handle "/*/" and "/**/"
275                        regex = regex.replaceAll(PATH_SEPARATOR + "\\*" + PATH_SEPARATOR, PATH_SEPARATOR +
276                                "[^" + PATH_SEPARATOR + "]+" + PATH_SEPARATOR);
277                        regex = regex.replaceAll(PATH_SEPARATOR + "[\\*]{2,}" + PATH_SEPARATOR, "(" +
278                                PATH_SEPARATOR + "|" + PATH_SEPARATOR + ".+" + PATH_SEPARATOR + ")");
279
280                        // Handle "*" within dir or file names
281                        regex = regex.replaceAll("\\*+", "[^" + PATH_SEPARATOR + "]*");
282
283                        // replace placeholder
284                        regex = Strings.replaceAll(regex, "#dot#", "\\.").toString();
285                        regex = Strings.replaceAll(regex, "#star#", "*").toString();
286
287                        return Pattern.compile(regex);
288                }
289
290                /**
291                 * Gets pattern.
292                 * 
293                 * @return pattern
294                 */
295                public String getPattern()
296                {
297                        return pattern;
298                }
299
300                /**
301                 * Gets regex.
302                 * 
303                 * @return regex
304                 */
305                public Pattern getRegex()
306                {
307                        return regex;
308                }
309
310                /**
311                 * Sets pattern.
312                 * 
313                 * @param pattern
314                 *            pattern
315                 */
316                public void setPattern(String pattern)
317                {
318                        if (Strings.isEmpty(pattern))
319                        {
320                                throw new IllegalArgumentException(
321                                        "Parameter 'pattern' can not be null or an empty string");
322                        }
323
324                        if (pattern.charAt(0) == '+')
325                        {
326                                include = true;
327                        }
328                        else if (pattern.charAt(0) == '-')
329                        {
330                                include = false;
331                        }
332                        else
333                        {
334                                throw new IllegalArgumentException(
335                                        "Parameter 'pattern' must start with either '+' or '-'. pattern='" + pattern +
336                                                "'");
337                        }
338
339                        this.pattern = pattern;
340                        regex = convertToRegex(pattern.substring(1));
341
342                        fileOnly = (pattern.indexOf(PATH_SEPARATOR) == -1);
343                }
344
345                /**
346                 * 
347                 * @param path
348                 * @return True if 'path' matches the pattern
349                 */
350                public boolean matches(String path)
351                {
352                        if (fileOnly)
353                        {
354                                path = Strings.lastPathComponent(path, PATH_SEPARATOR);
355                        }
356                        return regex.matcher(path).matches();
357                }
358
359                /**
360                 * Gets include.
361                 * 
362                 * @return include
363                 */
364                public boolean isInclude()
365                {
366                        return include;
367                }
368
369                /**
370                 * Sets include.
371                 * 
372                 * @param include
373                 *            include
374                 */
375                public void setInclude(boolean include)
376                {
377                        this.include = include;
378                }
379
380                /**
381                 * Gets active.
382                 * 
383                 * @return active
384                 */
385                public boolean isActive()
386                {
387                        return active;
388                }
389
390                /**
391                 * Sets active.
392                 * 
393                 * @param active
394                 *            active
395                 */
396                public void setActive(boolean active)
397                {
398                        this.active = active;
399                }
400
401                @Override
402                public String toString()
403                {
404                        return "Pattern: " + pattern + ", Regex: " + regex + ", include:" + include +
405                                ", fileOnly:" + fileOnly + ", active:" + active;
406                }
407        }
408
409        /**
410         * A very simple cache
411         */
412        public static class SimpleCache extends ConcurrentHashMap<String, Boolean>
413        {
414                private static final long serialVersionUID = 1L;
415
416                private final ConcurrentLinkedQueue<String> fifo = new ConcurrentLinkedQueue<>();
417
418                private final int maxSize;
419
420                /**
421                 * Construct.
422                 * 
423                 * @param maxSize
424                 */
425                public SimpleCache(int maxSize)
426                {
427                        this.maxSize = maxSize;
428                }
429
430                /**
431                 * @see java.util.concurrent.ConcurrentHashMap#put(java.lang.Object, java.lang.Object)
432                 */
433                @Override
434                public Boolean put(String key, Boolean value)
435                {
436                        // add the key to the hash map. Do not replace existing once
437                        Boolean rtn = super.putIfAbsent(key, value);
438
439                        // If found, than remove it from the fifo list and ...
440                        if (rtn != null)
441                        {
442                                fifo.remove(key);
443                        }
444
445                        // append it at the end of the list
446                        fifo.add(key);
447
448                        // remove all "outdated" cache entries
449                        while (fifo.size() > maxSize)
450                        {
451                                remove(fifo.poll());
452                        }
453                        return rtn;
454                }
455        }
456}