blob: 1c3cafea046909a46c6f8171f066da1d97e08448 [file] [log] [blame]
// 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.google.gerrit.httpd;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.server.plugins.StopPluginListener;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.servlet.ServletModule;
import java.io.IOException;
import java.util.Iterator;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/** Filters all HTTP requests passing through the server. */
public abstract class AllRequestFilter implements Filter {
public static Module module() {
return new ServletModule() {
@Override
protected void configureServlets() {
DynamicSet.setOf(binder(), AllRequestFilter.class);
DynamicSet.bind(binder(), AllRequestFilter.class)
.to(AllowRenderInFrameFilter.class)
.in(Scopes.SINGLETON);
filter("/*").through(FilterProxy.class);
bind(StopPluginListener.class)
.annotatedWith(UniqueAnnotations.create())
.to(FilterProxy.class);
}
};
}
@Singleton
static class FilterProxy implements Filter, StopPluginListener {
private final DynamicSet<AllRequestFilter> filters;
private DynamicSet<AllRequestFilter> initializedFilters;
private FilterConfig filterConfig;
@Inject
FilterProxy(DynamicSet<AllRequestFilter> filters) {
this.filters = filters;
this.initializedFilters = new DynamicSet<>();
this.filterConfig = null;
}
/**
* Initializes a filter if needed
*
* @param filter The filter that should get initialized
* @return {@code true} iff filter is now initialized
* @throws ServletException if filter itself fails to init
*/
private synchronized boolean initFilterIfNeeded(AllRequestFilter filter)
throws ServletException {
boolean ret = true;
if (filters.contains(filter)) {
// Regardless of whether or not the caller checked filter's
// containment in initializedFilters, we better re-check as we're now
// synchronized.
if (!initializedFilters.contains(filter)) {
filter.init(filterConfig);
initializedFilters.add("gerrit", filter);
}
} else {
ret = false;
}
return ret;
}
private synchronized void cleanUpInitializedFilters() {
Iterable<AllRequestFilter> filtersToCleanUp = initializedFilters;
initializedFilters = new DynamicSet<>();
for (AllRequestFilter filter : filtersToCleanUp) {
if (filters.contains(filter)) {
initializedFilters.add("gerrit", filter);
} else {
filter.destroy();
}
}
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain last)
throws IOException, ServletException {
final Iterator<AllRequestFilter> itr = filters.iterator();
new FilterChain() {
@Override
public void doFilter(ServletRequest req, ServletResponse res)
throws IOException, ServletException {
while (itr.hasNext()) {
AllRequestFilter filter = itr.next();
// To avoid {@code synchronized} on the whole filtering (and
// thereby killing concurrency), we start the below disjunction
// with an unsynchronized check for containment. This
// unsynchronized check is always correct if no filters got
// initialized/cleaned concurrently behind our back.
// The case of concurrently initialized filters is saved by the
// call to initFilterIfNeeded. So that's fine too.
// The case of concurrently cleaned filters between the {@code if}
// condition and the call to {@code doFilter} is not saved by
// anything. If a filter is getting removed concurrently while
// another thread is in those two lines, doFilter might (but need
// not) fail.
//
// Since this failure only occurs if a filter is deleted
// (e.g.: a plugin reloaded) exactly when a thread is in those
// two lines, and it only breaks a single request, we're ok with
// it, given that this is really both really improbable and also
// the "proper" fix for it would basically kill concurrency of
// webrequests.
if (initializedFilters.contains(filter) || initFilterIfNeeded(filter)) {
filter.doFilter(req, res, this);
return;
}
}
last.doFilter(req, res);
}
}.doFilter(req, res);
}
@Override
public void init(FilterConfig config) throws ServletException {
// Plugins that provide AllRequestFilters might get loaded later at
// runtime, long after this init method had been called. To allow to
// correctly init such plugins' AllRequestFilters, we keep the
// FilterConfig around, and reuse it to lazy init the AllRequestFilters.
filterConfig = config;
for (AllRequestFilter f : filters) {
initFilterIfNeeded(f);
}
}
@Override
public synchronized void destroy() {
Iterable<AllRequestFilter> filtersToDestroy = initializedFilters;
initializedFilters = new DynamicSet<>();
for (AllRequestFilter filter : filtersToDestroy) {
filter.destroy();
}
}
@Override
public void onStopPlugin(Plugin plugin) {
// In order to allow properly garbage collection, we need to scrub
// initializedFilters clean of filters stemming from plugins as they
// get unloaded.
cleanUpInitializedFilters();
}
}
@Override
public void init(FilterConfig config) throws ServletException {}
@Override
public void destroy() {}
}