CB-5615 dynamic cookie lifetime (#2956)

* CB-5615 dynamic cookie lifetime
This commit is contained in:
Alexander Skoblikov
2024-10-07 16:20:48 +00:00
committed by GitHub
parent 0850b2d94f
commit 16cd103585
3 changed files with 25 additions and 165 deletions
@@ -109,6 +109,8 @@ public abstract class CBApplication<T extends CBServerConfig> extends
private final Map<String, String> initActions = new ConcurrentHashMap<>();
private CBJettyServer jettyServer;
public CBApplication() {
this.homeDirectory = new File(initHomeFolder());
}
@@ -465,7 +467,8 @@ public abstract class CBApplication<T extends CBServerConfig> extends
getServerPort(),
CommonUtils.isEmpty(getServerHost()) ? "all interfaces" : getServerHost())
);
new CBJettyServer(this).runServer();
this.jettyServer = new CBJettyServer(this);
this.jettyServer.runServer();
}
@@ -565,6 +568,9 @@ public abstract class CBApplication<T extends CBServerConfig> extends
sendConfigChangedEvent(credentialsProvider);
eventController.setForceSkipEvents(isConfigurationMode());
if (this.jettyServer != null) {
this.jettyServer.refreshJettyConfig();
}
}
protected abstract void finishSecurityServiceConfiguration(
@@ -61,6 +61,7 @@ public class CBJettyServer {
}
private final CBApplication<?> application;
private Server server;
public CBJettyServer(@NotNull CBApplication<?> application) {
this.application = application;
@@ -69,7 +70,6 @@ public class CBJettyServer {
public void runServer() {
try {
CBServerConfig serverConfiguration = application.getServerConfiguration();
Server server;
int serverPort = serverConfiguration.getServerPort();
String serverHost = serverConfiguration.getServerHost();
Path sslPath = getSslConfigurationPath();
@@ -198,7 +198,7 @@ public class CBJettyServer {
}
}
}
refreshJettyConfig();
server.start();
server.join();
} catch (Exception e) {
@@ -224,6 +224,7 @@ public class CBJettyServer {
) {
// Init sessions persistence
CBSessionHandler sessionHandler = new CBSessionHandler(application);
sessionHandler.setRefreshCookieAge(CBSessionHandler.ONE_MINUTE);
int intMaxIdleSeconds;
if (maxIdleTime > Integer.MAX_VALUE) {
log.warn("Max session idle time value is greater than Integer.MAX_VALUE. Integer.MAX_VALUE will be used instead");
@@ -232,6 +233,7 @@ public class CBJettyServer {
intMaxIdleSeconds = (int) (maxIdleTime / 1000);
log.debug("Max http session idle time: " + intMaxIdleSeconds + "s");
sessionHandler.setMaxInactiveInterval(intMaxIdleSeconds);
sessionHandler.setMaxCookieAge(intMaxIdleSeconds);
DefaultSessionCache sessionCache = new DefaultSessionCache(sessionHandler);
sessionCache.setSessionDataStore(new NullSessionDataStore());
@@ -241,6 +243,19 @@ public class CBJettyServer {
DefaultSessionIdManager idMgr = new DefaultSessionIdManager(server);
idMgr.setWorkerName(null);
server.addBean(idMgr, true);
}
public synchronized void refreshJettyConfig() {
if (server == null) {
return;
}
log.info("Refreshing Jetty configuration");
if (server.getHandler() instanceof ServletContextHandler servletContextHandler
&& servletContextHandler.getSessionHandler() instanceof CBSessionHandler cbSessionHandler
) {
cbSessionHandler.setMaxCookieAge((int) (application.getMaxSessionIdleTime() / 1000));
var serverUrl = this.application.getServerURL();
cbSessionHandler.setSecureCookies(serverUrl != null && serverUrl.startsWith("https://"));
}
}
}
@@ -17,174 +17,13 @@
package io.cloudbeaver.server.jetty;
import io.cloudbeaver.server.GQLApplicationAdapter;
import jakarta.servlet.SessionCookieConfig;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.SessionHandler;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
public class CBSessionHandler extends SessionHandler {
private final CBCookieConfig cbCookieConfig;
static final int ONE_MINUTE = 60;
private final GQLApplicationAdapter application;
public CBSessionHandler(GQLApplicationAdapter application) {
this.cbCookieConfig = new CBCookieConfig();
this.application = application;
}
@Override
public SessionCookieConfig getSessionCookieConfig() {
return this.cbCookieConfig;
}
//mostly copy of org.eclipse.jetty.ee10.servlet.CookieConfig but allows to use dynamic setSecure flag
public final class CBCookieConfig implements SessionCookieConfig {
@Override
public boolean isSecure() {
var serverUrl = CBSessionHandler.this.application.getServerURL();
return serverUrl != null && serverUrl.startsWith("https://");
}
@Override
public String getComment() {
return getSessionComment();
}
@Override
public String getDomain() {
return getSessionDomain();
}
@Override
public int getMaxAge() {
return getMaxCookieAge();
}
@Override
public void setAttribute(String name, String value) {
checkState();
String lcase = name.toLowerCase(Locale.ENGLISH);
switch (lcase) {
case "name" -> setName(value);
case "max-age" -> setMaxAge(value == null ? -1 : Integer.parseInt(value));
case "comment" -> setComment(value);
case "domain" -> setDomain(value);
case "httponly" -> setHttpOnly(Boolean.parseBoolean(value));
case "secure" -> setSecure(Boolean.parseBoolean(value));
case "path" -> setPath(value);
default -> setSessionCookieAttribute(name, value);
}
}
@Override
public String getAttribute(String name) {
String lcase = name.toLowerCase(Locale.ENGLISH);
return switch (lcase) {
case "name" -> getName();
case "max-age" -> Integer.toString(getMaxAge());
case "comment" -> getComment();
case "domain" -> getDomain();
case "httponly" -> String.valueOf(isHttpOnly());
case "secure" -> String.valueOf(isSecure());
case "path" -> getPath();
default -> getSessionCookieAttribute(name);
};
}
/**
* According to the SessionCookieConfig javadoc, the attributes must also include
* all values set by explicit setters.
*
* @see SessionCookieConfig
*/
@Override
public Map<String, String> getAttributes() {
Map<String, String> specials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
specials.put("name", getAttribute("name"));
specials.put("max-age", getAttribute("max-age"));
specials.put("comment", getAttribute("comment"));
specials.put("domain", getAttribute("domain"));
specials.put("httponly", getAttribute("httponly"));
specials.put("secure", getAttribute("secure"));
specials.put("path", getAttribute("path"));
specials.putAll(getSessionCookieAttributes());
return Collections.unmodifiableMap(specials);
}
@Override
public String getName() {
return getSessionCookie();
}
@Override
public String getPath() {
return getSessionPath();
}
@Override
public boolean isHttpOnly() {
return CBSessionHandler.this.isHttpOnly();
}
@Override
public void setComment(String comment) {
checkState();
CBSessionHandler.this.setSessionComment(comment);
}
@Override
public void setDomain(String domain) {
checkState();
CBSessionHandler.this.setSessionDomain(domain);
}
@Override
public void setHttpOnly(boolean httpOnly) {
checkState();
CBSessionHandler.this.setHttpOnly(httpOnly);
}
@Override
public void setMaxAge(int maxAge) {
checkState();
CBSessionHandler.this.setMaxCookieAge(maxAge);
}
@Override
public void setName(String name) {
checkState();
CBSessionHandler.this.setSessionCookie(name);
}
@Override
public void setPath(String path) {
checkState();
CBSessionHandler.this.setSessionPath(path);
}
@Override
public void setSecure(boolean secure) {
checkState();
CBSessionHandler.this.setSecureCookies(secure);
}
private void checkState() {
//It is allowable to call the CookieConfig.setXX methods after the SessionHandler has started,
//but before the context has fully started. Ie it is allowable for ServletContextListeners
//to call these methods in contextInitialized().
ServletContextHandler handler = ServletContextHandler.getCurrentServletContextHandler();
if (handler != null && handler.isAvailable())
throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started");
}
}
}