// Copyright (C) 2012 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.googlesource.gerrit.plugins.gitblit.app; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpSession; import org.apache.wicket.IRequestTarget; import org.apache.wicket.RequestCycle; import org.apache.wicket.Session; import org.apache.wicket.markup.html.IHeaderResponse; import org.apache.wicket.markup.html.IHeaderResponseDecorator; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.protocol.http.WebRequest; import org.apache.wicket.protocol.http.WebRequestCycle; import org.apache.wicket.protocol.http.WebRequestCycleProcessor; import org.apache.wicket.request.IRequestCodingStrategy; import org.apache.wicket.request.IRequestCycleProcessor; import org.apache.wicket.request.RequestParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.Keys; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.manager.IFederationManager; import com.gitblit.manager.IFilestoreManager; import com.gitblit.manager.IGitblit; import com.gitblit.manager.INotificationManager; import com.gitblit.manager.IPluginManager; import com.gitblit.manager.IProjectManager; import com.gitblit.manager.IRepositoryManager; import com.gitblit.manager.IRuntimeManager; import com.gitblit.manager.IServicesManager; import com.gitblit.manager.IUserManager; import com.gitblit.tickets.ITicketService; import com.gitblit.transport.ssh.IPublicKeyManager; import com.gitblit.wicket.CacheControl; import com.gitblit.wicket.GitBlitWebApp; import com.gitblit.wicket.GitBlitWebSession; import com.gitblit.wicket.GitblitParamUrlCodingStrategy; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.httpd.WebSession; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @Singleton public class GerritGitBlitWebApp extends GitBlitWebApp { private static final Logger log = LoggerFactory.getLogger(GerritGitBlitWebApp.class); private static final String INSTANCE_ATTRIBUTE = "GerritGitBlitPluginInstance"; private final DynamicItem<WebSession> gerritSesssion; // We have to re-implement this bit from the super class because we have to override mount below because the XSS filtering in the // GitblitParamUrlCodingStrategy is wrong (it triggers on perfectly harmless UTF-8 characters like à). Luckily this cacheablePages // is not accessed locally in the super class except in two getters, which we also override. private final Map<String, CacheControl> cacheablePages = new HashMap<String, CacheControl>(); /** A key that is unique for each plugin instance. */ private String pluginInstanceKey; @Inject public GerritGitBlitWebApp(Provider<IPublicKeyManager> publicKeyManagerProvider, Provider<ITicketService> ticketServiceProvider, IRuntimeManager runtimeManager, IPluginManager pluginManager, INotificationManager notificationManager, IUserManager userManager, IAuthenticationManager authenticationManager, IRepositoryManager repositoryManager, IProjectManager projectManager, IFederationManager federationManager, IGitblit gitblit, IServicesManager services, IFilestoreManager filestoreManager, DynamicItem<WebSession> gerritSession) { super(publicKeyManagerProvider, ticketServiceProvider, runtimeManager, pluginManager, notificationManager, userManager, authenticationManager, repositoryManager, projectManager, federationManager, gitblit, services, filestoreManager); this.gerritSesssion = gerritSession; // We need this, otherwise the flotr2 library adds again links that are not recoded for static access. setHeaderResponseDecorator(new IHeaderResponseDecorator() { @Override public IHeaderResponse decorate(IHeaderResponse response) { return new StaticRewritingHeaderResponse(response); } }); } /** * Sets a unique key that is different for each plugin instance (i.e., different for each load of the plugin.) * * @param key * for this plugin instance. */ public void setPluginInstanceKey(String key) { log.info("Instance key = {}", key); this.pluginInstanceKey = key; } public String getPluginInstanceKey() { return pluginInstanceKey; } @Override public void mount(String location, Class<? extends WebPage> clazz, String... parameters) { if (parameters == null || !settings().getBoolean(Keys.web.mountParameters, true)) { parameters = new String[0]; } mount(new GitblitParamUrlCodingStrategy(settings(), new NullXssFilter(), location, clazz, parameters)); // map the mount point to the cache control definition if (clazz.isAnnotationPresent(CacheControl.class)) { CacheControl cacheControl = clazz.getAnnotation(CacheControl.class); cacheablePages.put(location.substring(1), cacheControl); } } @Override public boolean isCacheablePage(String mountPoint) { return cacheablePages.containsKey(mountPoint); } @Override public CacheControl getCacheControl(String mountPoint) { return cacheablePages.get(mountPoint); } @Override protected IRequestCycleProcessor newRequestCycleProcessor() { return new WebRequestCycleProcessor() { @Override public IRequestTarget resolve(RequestCycle requestCycle, RequestParameters requestParameters) { if (requestCycle instanceof WebRequestCycle) { resetWicketSessionOnPluginReload((WebRequestCycle) requestCycle); } // If the user logged out in Gerrit, we must tell GitBlit's Wicket session about it. GitBlitWebSession wicketSession; // For some reason that's unclear to me we still sometimes get a ClassCastException here after // a plugin reload. Possibly ThreadLocals also survive plugin reload? try { wicketSession = GitBlitWebSession.get(); } catch (ClassCastException ex) { if (requestCycle instanceof WebRequestCycle) { log.info("Force cleanup"); forceCleanup((WebRequestCycle) requestCycle); } else { log.warn("Not a web request: {}", requestCycle.getClass().getName()); Session.unset(); // reset ThreadLocal } wicketSession = GitBlitWebSession.get(); } if (wicketSession.isLoggedIn() && !gerritSesssion.get().isSignedIn()) { wicketSession.replaceSession(); wicketSession.setUser(null); } return super.resolve(requestCycle, requestParameters); } private void resetWicketSessionOnPluginReload(WebRequestCycle requestCycle) { WebRequest request = requestCycle.getWebRequest(); HttpSession realSession = request.getHttpServletRequest().getSession(); if (realSession != null) { Object sessionPluginInstanceKey = realSession.getAttribute(INSTANCE_ATTRIBUTE); String currentPluginInstanceKey = getPluginInstanceKey(); if (sessionPluginInstanceKey instanceof String) { if (!sessionPluginInstanceKey.equals(currentPluginInstanceKey)) { cleanUp(realSession, request, currentPluginInstanceKey); } } else { Session.unset(); realSession.setAttribute(INSTANCE_ATTRIBUTE, currentPluginInstanceKey); } } else { // Should not occur. log.warn("No HTTP session"); Session.unset(); } } private void forceCleanup(WebRequestCycle requestCycle) { WebRequest request = requestCycle.getWebRequest(); HttpSession realSession = request.getHttpServletRequest().getSession(); if (realSession != null) { cleanUp(realSession, request, getPluginInstanceKey()); } else { log.warn("No HTTPSession in force cleanup"); Session.unset(); } } private void cleanUp(HttpSession realSession, WebRequest request, String currentPluginInstanceKey) { log.info("Clean up after plugin reload"); // Plugin was restarted during the session. Wicket has stored unserialized Java objects in this session // object. We must remove them, so that Wicket creates a new Wicket session, otherwise we end up with // ClassCastExceptions further down the line. We mustn't try to get the session and invalidate or replace // it; we must just shoot it dead by removing it from the real HTTP session object. for (String name : getSessionStore().getAttributeNames(request)) { log.info("Removing {}", name); getSessionStore().removeAttribute(request, name); } // Also remove the session unbinding listener. It's already too late to properly do anything with this // orphan object that has a class that nobody knows anymore. realSession.removeAttribute("Wicket:SessionUnbindingListener-" + getApplicationKey()); Session.unset(); // The above is a bit very hacky. A Wicket Guru might know a cleaner way, but I don't. realSession.setAttribute(INSTANCE_ATTRIBUTE, currentPluginInstanceKey); } @Override protected IRequestCodingStrategy newRequestCodingStrategy() { return new StaticCodingStrategy("summary/", "project/"); } }; } }