package io.vertx.ext.web.handler.impl;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.CookieSameSite;
import io.vertx.core.impl.logging.Logger;
import io.vertx.core.impl.logging.LoggerFactory;
import io.vertx.ext.auth.AuthProvider;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.Session;
import io.vertx.ext.web.handler.SessionHandler;
import io.vertx.ext.web.sstore.SessionStore;
import io.vertx.ext.web.sstore.impl.SessionInternal;
public class SessionHandlerImpl implements SessionHandler {
private static final String SESSION_USER_HOLDER_KEY = "__vertx.userHolder";
private static final String SESSION_FLUSHED_KEY = "__vertx.session-flushed";
private static final String SESSION_STOREUSER_KEY = "__vertx.session-storeuser";
private static final Logger log = LoggerFactory.getLogger(SessionHandlerImpl.class);
private final SessionStore sessionStore;
private String sessionCookieName = DEFAULT_SESSION_COOKIE_NAME;
private String sessionCookiePath = DEFAULT_SESSION_COOKIE_PATH;
private long sessionTimeout = DEFAULT_SESSION_TIMEOUT;
private boolean nagHttps = DEFAULT_NAG_HTTPS;
private boolean sessionCookieSecure = DEFAULT_COOKIE_SECURE_FLAG;
private boolean sessionCookieHttpOnly = DEFAULT_COOKIE_HTTP_ONLY_FLAG;
private int minLength = DEFAULT_SESSIONID_MIN_LENGTH;
private boolean lazySession = DEFAULT_LAZY_SESSION;
private long cookieMaxAge = -1;
private boolean cookieless;
private CookieSameSite cookieSameSite;
public SessionHandlerImpl(SessionStore sessionStore) {
this.sessionStore = sessionStore;
}
@Override
public SessionHandler setSessionTimeout(long timeout) {
this.sessionTimeout = timeout;
return this;
}
@Override
public SessionHandler setNagHttps(boolean nag) {
this.nagHttps = nag;
return this;
}
@Override
public SessionHandler setCookieSecureFlag(boolean secure) {
this.sessionCookieSecure = secure;
return this;
}
@Override
public SessionHandler setCookieHttpOnlyFlag(boolean httpOnly) {
this.sessionCookieHttpOnly = httpOnly;
return this;
}
@Override
public SessionHandler setSessionCookieName(String sessionCookieName) {
this.sessionCookieName = sessionCookieName;
return this;
}
@Override
public SessionHandler setSessionCookiePath(String sessionCookiePath) {
this.sessionCookiePath = sessionCookiePath;
return this;
}
@Override
public SessionHandler setMinLength(int minLength) {
this.minLength = minLength;
return this;
}
@Override
public SessionHandler setCookieSameSite(CookieSameSite policy) {
this.cookieSameSite = policy;
return this;
}
@Override
public SessionHandler setLazySession(boolean lazySession) {
this.lazySession = lazySession;
return this;
}
@Override
public SessionHandler setCookieMaxAge(long cookieMaxAge) {
this.cookieMaxAge = cookieMaxAge;
return this;
}
@Override
@Deprecated
public SessionHandler setAuthProvider(AuthProvider authProvider) {
return this;
}
@Override
public SessionHandler setCookieless(boolean cookieless) {
this.cookieless = cookieless;
return this;
}
@Override
public SessionHandler flush(RoutingContext context, Handler<AsyncResult<Void>> handler) {
return flush(context, false, handler);
}
private void setCookieProperties(Cookie cookie) {
cookie.setPath(sessionCookiePath);
cookie.setSecure(sessionCookieSecure);
cookie.setHttpOnly(sessionCookieHttpOnly);
cookie.setSameSite(cookieSameSite);
if (cookieMaxAge >= 0) {
cookie.setMaxAge(cookieMaxAge);
}
}
private SessionHandler flush(RoutingContext context, boolean skipCrc, Handler<AsyncResult<Void>> handler) {
boolean sessionUsed = context.isSessionAccessed();
Session session = context.session();
if (!session.isDestroyed()) {
final int currentStatusCode = context.response().getStatusCode();
if (currentStatusCode >= 200 && currentStatusCode < 400) {
Boolean storeUser = context.get(SESSION_STOREUSER_KEY);
if (storeUser != null && storeUser) {
if (context.user() != null) {
session.put(SESSION_USER_HOLDER_KEY, new UserHolder(context));
}
}
if (session.isRegenerated()) {
if (cookieless) {
session.setAccessed();
} else {
final Cookie cookie = sessionCookie(context, session);
session.setAccessed();
cookie.setValue(session.value());
setCookieProperties(cookie);
}
sessionStore.delete(session.oldId(), delete -> {
if (delete.failed()) {
handler.handle(Future.failedFuture(delete.cause()));
} else {
sessionStore.put(session, put -> {
if (put.failed()) {
handler.handle(Future.failedFuture(put.cause()));
} else {
context.put(SESSION_FLUSHED_KEY, true);
if (session instanceof SessionInternal) {
((SessionInternal) session).flushed(skipCrc);
}
handler.handle(Future.succeededFuture());
}
});
}
});
} else if (!lazySession || sessionUsed) {
if (!cookieless) {
sessionCookie(context, session);
}
session.setAccessed();
sessionStore.put(session, put -> {
if (put.failed()) {
handler.handle(Future.failedFuture(put.cause()));
} else {
context.put(SESSION_FLUSHED_KEY, true);
if (session instanceof SessionInternal) {
((SessionInternal) session).flushed(skipCrc);
}
handler.handle(Future.succeededFuture());
}
});
}
}
} else {
if (!cookieless) {
context.removeCookie(sessionCookieName);
}
if (session.isRegenerated()) {
sessionStore.delete(session.oldId(), delete -> {
if (delete.failed()) {
handler.handle(Future.failedFuture(delete.cause()));
} else {
sessionStore.delete(session.id(), delete2 -> {
if (delete2.failed()) {
handler.handle(Future.failedFuture(delete2.cause()));
} else {
context.put(SESSION_FLUSHED_KEY, true);
handler.handle(Future.succeededFuture());
}
});
}
});
} else {
sessionStore.delete(session.id(), delete -> {
if (delete.failed()) {
handler.handle(Future.failedFuture(delete.cause()));
} else {
context.put(SESSION_FLUSHED_KEY, true);
handler.handle(Future.succeededFuture());
}
});
}
}
return this;
}
@Override
public void handle(RoutingContext context) {
if (nagHttps && log.isDebugEnabled()) {
String uri = context.request().absoluteURI();
if (!uri.startsWith("https:")) {
log.debug(
"Using session cookies without https could make you susceptible to session hijacking: " + uri);
}
}
String sessionID = getSessionId(context);
if (sessionID != null && sessionID.length() > minLength) {
getSession(context.vertx(), sessionID, res -> {
if (res.succeeded()) {
Session session = res.result();
if (session != null) {
context.setSession(session);
UserHolder holder = session.get(SESSION_USER_HOLDER_KEY);
if (holder != null) {
holder.refresh(context);
} else {
context.put(SESSION_STOREUSER_KEY, true);
}
addStoreSessionHandler(context);
} else {
createNewSession(context);
}
} else {
context.fail(res.cause());
}
context.next();
});
} else {
createNewSession(context);
context.next();
}
}
private String getSessionId(RoutingContext context) {
if (cookieless) {
String path = context.normalizedPath();
int s = -1;
int e = -1;
for (int i = 0; i < path.length(); i++) {
if (path.charAt(i) == '(') {
s = i + 1;
continue;
}
if (path.charAt(i) == ')') {
if (s != -1) {
e = i;
break;
}
}
}
if (s != -1 && e != -1 && s < e) {
return path.substring(s, e);
}
} else {
Cookie cookie = context.getCookie(sessionCookieName);
if (cookie != null) {
return cookie.getValue();
}
}
return null;
}
private void getSession(Vertx vertx, String sessionID, Handler<AsyncResult<Session>> resultHandler) {
doGetSession(vertx, System.currentTimeMillis(), sessionID, resultHandler);
}
private void doGetSession(Vertx vertx, long startTime, String sessionID, Handler<AsyncResult<Session>> resultHandler) {
sessionStore.get(sessionID, res -> {
if (res.succeeded()) {
if (res.result() == null) {
long retryTimeout = sessionStore.retryTimeout();
if (retryTimeout > 0 && System.currentTimeMillis() - startTime < retryTimeout) {
vertx.setTimer(5, v -> doGetSession(vertx, startTime, sessionID, resultHandler));
return;
}
}
}
resultHandler.handle(res);
});
}
private void addStoreSessionHandler(RoutingContext context) {
context.addHeadersEndHandler(v -> {
Boolean flushed = context.get(SESSION_FLUSHED_KEY);
if (flushed == null || !flushed) {
flush(context, true, flush -> {
if (flush.failed()) {
log.warn("Failed to flush the session to the underlying store", flush.cause());
}
});
}
});
}
private void createNewSession(RoutingContext context) {
Session session = sessionStore.createSession(sessionTimeout, minLength);
context.setSession(session);
if (!cookieless) {
context.removeCookie(sessionCookieName, false);
}
context.put(SESSION_STOREUSER_KEY, true);
addStoreSessionHandler(context);
}
private Cookie sessionCookie(final RoutingContext context, final Session session) {
Cookie cookie = context.getCookie(sessionCookieName);
if (cookie != null) {
return cookie;
}
cookie = Cookie.cookie(sessionCookieName, session.value());
setCookieProperties(cookie);
context.addCookie(cookie);
return cookie;
}
}