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.protocol.https;
018
019import jakarta.servlet.http.HttpServletRequest;
020
021import org.apache.wicket.Session;
022import org.apache.wicket.core.request.handler.IPageClassRequestHandler;
023import org.apache.wicket.request.IRequestCycle;
024import org.apache.wicket.request.IRequestHandler;
025import org.apache.wicket.request.IRequestMapper;
026import org.apache.wicket.request.Request;
027import org.apache.wicket.request.Url;
028import org.apache.wicket.request.component.IRequestablePage;
029import org.apache.wicket.request.cycle.RequestCycle;
030import org.apache.wicket.request.http.WebResponse;
031import org.apache.wicket.request.mapper.IRequestMapperDelegate;
032import org.apache.wicket.util.collections.ClassMetaCache;
033import org.apache.wicket.util.lang.Args;
034
035/**
036 * A {@link IRequestMapper} that will issue a redirect to secured communication (over https) if the
037 * page resolved by {@linkplain #delegate} is annotated with @{@link RequireHttps}
038 * 
039 * <p>
040 * To setup it:
041 * 
042 * <pre>
043 * public class MyApplication extends WebApplication
044 * {
045 *      public void init()
046 *      {
047 *              super.init();
048 * 
049 *              getRootRequestMapperAsCompound().add(new MountedMapper(&quot;secured&quot;, HttpsPage.class));
050 *              mountPage(SomeOtherPage.class);
051 * 
052 *              // notice that in most cases this should be done as the
053 *              // last mounting-related operation because it replaces the root mapper
054 *              setRootRequestMapper(new HttpsMapper(getRootRequestMapper(), new HttpsConfig(80, 443)));
055 *      }
056 * }
057 * </pre>
058 * 
059 * any request to <em>http://hostname:httpPort/secured</em> will be redirected to
060 * <em>https://hostname:httpsPort/secured</em>
061 * 
062 * @author igor
063 */
064public class HttpsMapper implements IRequestMapperDelegate
065{
066        private final HttpsConfig config;
067        private final IRequestMapper delegate;
068        private final ClassMetaCache<Scheme> cache = new ClassMetaCache<Scheme>();
069
070        /**
071         * Constructor
072         * 
073         * @param delegate
074         * @param config
075         */
076        public HttpsMapper(IRequestMapper delegate, HttpsConfig config)
077        {
078                this.delegate = Args.notNull(delegate, "delegate");
079                this.config = config;
080        }
081
082        /**
083         * {@inheritDoc}
084         */
085        @Override
086        public IRequestMapper getDelegateMapper()
087        {
088                return delegate;
089        }
090
091        @Override
092        public final int getCompatibilityScore(Request request)
093        {
094                return delegate.getCompatibilityScore(request);
095        }
096
097
098        @Override
099        public final IRequestHandler mapRequest(Request request)
100        {
101                IRequestHandler handler = delegate.mapRequest(request);
102
103                Scheme desired = getDesiredSchemeFor(handler);
104                if (Scheme.ANY.equals(desired))
105                {
106                        return handler;
107                }
108                
109                Scheme current = getSchemeOf(request);
110                if (!desired.isCompatibleWith(current))
111                {
112                        // we are currently on the wrong scheme for this handler
113
114                        // construct a url for the handler on the correct scheme
115                        String url = createRedirectUrl(handler, request, desired);
116
117                        // replace handler with one that will redirect to the created url
118                        handler = createRedirectHandler(url);
119                }
120                return handler;
121        }
122
123        @Override
124        public final Url mapHandler(IRequestHandler handler)
125        {
126                return mapHandler(handler, RequestCycle.get().getRequest());
127        }
128
129        /**
130         * Creates the {@link IRequestHandler} that will be responsible for the redirect
131         * 
132         * @param url
133         * @return request handler
134         */
135        protected IRequestHandler createRedirectHandler(String url)
136        {
137                return new RedirectHandler(url, config);
138        }
139
140        /**
141         * Constructs a redirect url that should switch the user to the specified {@code scheme}
142         * 
143         * @param handler
144         *            request handler being accessed
145         * @param request
146         *            current request
147         * @param scheme
148         *            desired scheme for the redirect url
149         * @return url
150         */
151        protected String createRedirectUrl(IRequestHandler handler, Request request, Scheme scheme)
152        {
153                HttpServletRequest req = (HttpServletRequest)request.getContainerRequest();
154                String url = scheme.urlName() + "://";
155                url += req.getServerName();
156                if (!scheme.usesStandardPort(config))
157                {
158                        url += ":" + scheme.getPort(config);
159                }
160                url += req.getRequestURI();
161                if (req.getQueryString() != null)
162                {
163                        url += "?" + req.getQueryString();
164                }
165                return url;
166        }
167
168
169        /**
170         * Creates a url for the handler. Modifies it with the correct {@link Scheme} if necessary.
171         * 
172         * @param handler
173         * @param request
174         * @return url
175         */
176        final Url mapHandler(IRequestHandler handler, Request request)
177        {
178                Url url = delegate.mapHandler(handler);
179
180                Scheme desired = getDesiredSchemeFor(handler);
181                if (Scheme.ANY.equals(desired))
182                {
183                        return url;
184                }
185                
186                Scheme current = getSchemeOf(request);
187                if (!desired.isCompatibleWith(current))
188                {
189                        // the generated url does not have the correct scheme, set it (which in turn will cause
190                        // the url to be rendered in its full representation)
191                        url.setProtocol(desired.urlName());
192                        url.setPort(desired.getPort(config));
193                }
194                return url;
195        }
196
197
198        /**
199         * Figures out which {@link Scheme} should be used to access the request handler
200         * 
201         * @param handler
202         *            request handler
203         * @return {@link Scheme}
204         */
205        protected Scheme getDesiredSchemeFor(IRequestHandler handler)
206        {
207                if (handler instanceof IPageClassRequestHandler)
208                {
209                        return getDesiredSchemeFor(((IPageClassRequestHandler)handler).getPageClass());
210                }
211                return Scheme.ANY;
212        }
213
214        /**
215         * Determines the {@link Scheme} of the request
216         * 
217         * @param request
218         * @return {@link Scheme#HTTPS} or {@link Scheme#HTTP}
219         */
220        protected Scheme getSchemeOf(Request request)
221        {
222                HttpServletRequest req = (HttpServletRequest) request.getContainerRequest();
223
224                if ("https".equalsIgnoreCase(req.getScheme()))
225                {
226                        return Scheme.HTTPS;
227                }
228                else if ("http".equalsIgnoreCase(req.getScheme()))
229                {
230                        return Scheme.HTTP;
231                }
232                else
233                {
234                        throw new IllegalStateException("Could not resolve protocol for request: " + req);
235                }
236        }
237
238        /**
239         * Determines which {@link Scheme} should be used to access the page
240         * 
241         * @param pageClass
242         *            type of page
243         * @return {@link Scheme}
244         */
245        protected Scheme getDesiredSchemeFor(Class<? extends IRequestablePage> pageClass)
246        {
247                if (pageClass == null)
248                {
249                        return Scheme.ANY;
250                }
251
252                Scheme SCHEME = cache.get(pageClass);
253                if (SCHEME == null)
254                {
255                        if (hasSecureAnnotation(pageClass))
256                        {
257                                SCHEME = Scheme.HTTPS;
258                        }
259                        else
260                        {
261                                SCHEME = Scheme.HTTP;
262                        }
263                        cache.put(pageClass, SCHEME);
264                }
265                return SCHEME;
266        }
267
268        /**
269         * @return config with which this mapper was created
270         */
271        public final HttpsConfig getConfig()
272        {
273                return config;
274        }
275
276        /**
277         * Checks if the specified {@code type} has the {@link RequireHttps} annotation
278         * 
279         * @param type
280         * @return {@code true} iff {@code type} has the {@link RequireHttps} annotation
281         */
282        private boolean hasSecureAnnotation(Class<?> type)
283        {
284                if (type.getAnnotation(RequireHttps.class) != null)
285                {
286                        return true;
287                }
288
289                for (Class<?> iface : type.getInterfaces())
290                {
291                        if (hasSecureAnnotation(iface))
292                        {
293                                return true;
294                        }
295                }
296
297                if (type.getSuperclass() != null)
298                {
299                        return hasSecureAnnotation(type.getSuperclass());
300                }
301                return false;
302        }
303
304
305        /**
306         * Handler that takes care of redirecting
307         * 
308         * @author igor
309         */
310        public static class RedirectHandler implements IRequestHandler
311        {
312                private final String url;
313                private final HttpsConfig config;
314
315                /**
316                 * Constructor
317                 * 
318                 * @param config
319                 *            https config
320                 * @param url
321                 *            redirect location
322                 */
323                public RedirectHandler(String url, HttpsConfig config)
324                {
325                        this.url = Args.notNull(url, "url");
326                        this.config = Args.notNull(config, "config");
327                }
328
329                /**
330                 * @return redirect location
331                 */
332                public String getUrl()
333                {
334                        return url;
335                }
336
337                @Override
338                public void respond(IRequestCycle requestCycle)
339                {
340                        String location = url;
341
342                        if (location.startsWith("/"))
343                        {
344                                // context-absolute url
345                                location = requestCycle.getUrlRenderer().renderContextRelativeUrl(location);
346                        }
347
348                        if (config.isPreferStateful())
349                        {
350                                // we need to persist the session before a redirect to https so the session lasts
351                                // across both http and https calls.
352                                Session.get().bind();
353                        }
354
355                        WebResponse response = (WebResponse)requestCycle.getResponse();
356                        response.sendRedirect(location);
357                }
358        }
359}