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 javax.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("secured", 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}