add user profile persistence, for adding user preferences

This commit is contained in:
Jörg Prante 2024-06-03 19:13:29 +02:00
parent b7417c9798
commit 1ad8f686df
23 changed files with 466 additions and 98 deletions

View file

@ -1,3 +1,3 @@
group = org.xbib group = org.xbib
name = net-http name = net-http
version = 4.6.0 version = 4.7.0

View file

@ -3,11 +3,13 @@ package org.xbib.net.http.server.application.web;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.Duration; import java.time.Duration;
import org.xbib.net.UserProfile;
import org.xbib.net.http.server.application.BaseApplication; import org.xbib.net.http.server.application.BaseApplication;
import org.xbib.net.http.server.route.HttpRouterContext; import org.xbib.net.http.server.route.HttpRouterContext;
import org.xbib.net.http.server.persist.Codec; import org.xbib.net.http.server.persist.Codec;
import org.xbib.net.http.server.session.Session; import org.xbib.net.http.server.session.Session;
import org.xbib.net.http.server.session.file.FileJsonSessionCodec; import org.xbib.net.http.server.session.FileJsonSessionCodec;
import org.xbib.net.http.server.auth.FileJsonUserProfileCodec;
public class WebApplication extends BaseApplication { public class WebApplication extends BaseApplication {
@ -28,4 +30,9 @@ public class WebApplication extends BaseApplication {
return new FileJsonSessionCodec(sessionName, this, 1024, Duration.ofDays(1), return new FileJsonSessionCodec(sessionName, this, 1024, Duration.ofDays(1),
Paths.get("/var/tmp/session")); Paths.get("/var/tmp/session"));
} }
@Override
protected Codec<UserProfile> newUserProfileCodec(HttpRouterContext httpRouterContext) {
return new FileJsonUserProfileCodec(Paths.get("/var/tmp/userprofile"));
}
} }

View file

@ -27,8 +27,6 @@ module org.xbib.net.http.server {
exports org.xbib.net.http.server.route; exports org.xbib.net.http.server.route;
exports org.xbib.net.http.server.service; exports org.xbib.net.http.server.service;
exports org.xbib.net.http.server.session; exports org.xbib.net.http.server.session;
exports org.xbib.net.http.server.session.file;
exports org.xbib.net.http.server.session.memory;
exports org.xbib.net.http.server.validate; exports org.xbib.net.http.server.validate;
exports org.xbib.net.http.server.executor; exports org.xbib.net.http.server.executor;
} }

View file

@ -15,6 +15,7 @@ import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.xbib.net.Attributes; import org.xbib.net.Attributes;
import org.xbib.net.UserProfile;
import org.xbib.net.http.HttpAddress; import org.xbib.net.http.HttpAddress;
import org.xbib.net.http.cookie.SameSite; import org.xbib.net.http.cookie.SameSite;
import org.xbib.net.http.server.auth.BaseAttributes; import org.xbib.net.http.server.auth.BaseAttributes;
@ -34,7 +35,8 @@ import org.xbib.net.http.server.session.IncomingContextHandler;
import org.xbib.net.http.server.session.OutgoingContextHandler; import org.xbib.net.http.server.session.OutgoingContextHandler;
import org.xbib.net.http.server.session.PersistSessionHandler; import org.xbib.net.http.server.session.PersistSessionHandler;
import org.xbib.net.http.server.session.Session; import org.xbib.net.http.server.session.Session;
import org.xbib.net.http.server.session.memory.MemoryPropertiesSessionCodec; import org.xbib.net.http.server.session.MemoryPropertiesSessionCodec;
import org.xbib.net.http.server.auth.MemoryPropertiesUserProfileCodec;
import org.xbib.net.http.server.validate.HttpRequestValidator; import org.xbib.net.http.server.validate.HttpRequestValidator;
import org.xbib.net.mime.MimeTypeService; import org.xbib.net.mime.MimeTypeService;
import org.xbib.net.util.RandomUtil; import org.xbib.net.util.RandomUtil;
@ -168,13 +170,15 @@ public class BaseApplication implements Application {
HttpRouterContext httpRouterContext = new BaseHttpRouterContext(this, domain, requestBuilder, responseBuilder); HttpRouterContext httpRouterContext = new BaseHttpRouterContext(this, domain, requestBuilder, responseBuilder);
httpRouterContext.addOpenHandler(newRequestValidator()); httpRouterContext.addOpenHandler(newRequestValidator());
httpRouterContext.addOpenHandler(newIncomingCookieHandler()); httpRouterContext.addOpenHandler(newIncomingCookieHandler());
if (builder.sessionsEnabled) { Codec<UserProfile> userProfileCodec = newUserProfileCodec(httpRouterContext);
Codec<Session> sessionCodec = newSessionCodec(httpRouterContext); httpRouterContext.getAttributes().put("userprofilecodec", userProfileCodec);
Codec<Session> sessionCodec = builder.sessionsEnabled ? newSessionCodec(httpRouterContext) : null;
if (sessionCodec != null) {
httpRouterContext.getAttributes().put("sessioncodec", sessionCodec); httpRouterContext.getAttributes().put("sessioncodec", sessionCodec);
httpRouterContext.addOpenHandler(newIncomingContextHandler(sessionCodec));
httpRouterContext.addCloseHandler(newOutgoingContextHandler());
httpRouterContext.addCloseHandler(newPersistHandler(sessionCodec));
} }
httpRouterContext.addOpenHandler(newIncomingContextHandler(userProfileCodec, sessionCodec));
httpRouterContext.addCloseHandler(newOutgoingContextHandler());
httpRouterContext.addCloseHandler(newPersistHandler(userProfileCodec, sessionCodec));
httpRouterContext.addCloseHandler(newOutgoingCookieHandler()); httpRouterContext.addCloseHandler(newOutgoingCookieHandler());
return httpRouterContext; return httpRouterContext;
} }
@ -212,8 +216,12 @@ public class BaseApplication implements Application {
return new MemoryPropertiesSessionCodec(sessionName,this, 1024, Duration.ofDays(1)); return new MemoryPropertiesSessionCodec(sessionName,this, 1024, Duration.ofDays(1));
} }
protected HttpHandler newIncomingContextHandler(Codec<Session> sessionCodec) { protected Codec<UserProfile> newUserProfileCodec(HttpRouterContext httpRouterContext) {
return new IncomingContextHandler( return new MemoryPropertiesUserProfileCodec();
}
protected HttpHandler newIncomingContextHandler(Codec<UserProfile> userProfileCodec, Codec<Session> sessionCodec) {
return new IncomingContextHandler(userProfileCodec,
getSecret(), getSecret(),
"HmacSHA1", "HmacSHA1",
sessionName, sessionName,
@ -235,8 +243,8 @@ public class BaseApplication implements Application {
); );
} }
protected HttpHandler newPersistHandler(Codec<Session> sessionCodec) { protected HttpHandler newPersistHandler(Codec<UserProfile> userProfileCodec, Codec<Session> sessionCodec) {
return new PersistSessionHandler(sessionCodec); return new PersistSessionHandler(userProfileCodec, sessionCodec);
} }
@Override @Override

View file

@ -0,0 +1,139 @@
package org.xbib.net.http.server.auth;
import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders;
import org.xbib.net.UserProfile;
import org.xbib.net.http.server.persist.Codec;
import org.xbib.net.util.JsonUtil;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
public class FileJsonUserProfileCodec implements Codec<UserProfile> {
private static final Logger logger = Logger.getLogger(FileJsonUserProfileCodec.class.getName());
private final ReentrantReadWriteLock lock;
private final Path path;
public FileJsonUserProfileCodec(Path path) {
this.path = path;
this.lock = new ReentrantReadWriteLock();
try {
Files.createDirectories(path);
} catch (IOException e) {
logger.log(Level.SEVERE, e.getMessage(), e);
throw new UncheckedIOException(e);
}
}
@Override
public UserProfile create(String key) throws IOException {
Objects.requireNonNull(key, "key must not be null");
BaseUserProfile baseUserProfile = new BaseUserProfile();
baseUserProfile.setUserId(key);
return baseUserProfile;
}
@Override
public UserProfile read(String key) throws IOException {
Objects.requireNonNull(key, "key must not be null");
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
try {
readLock.lock();
PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8);
Path p = path.resolve(percentEncoder.encode(key));
Map<String, Object> map = JsonUtil.toMap(Files.readString(p));
return BaseUserProfile.fromMap(map);
} finally {
readLock.unlock();
}
}
@Override
public void write(String key, UserProfile userProfile) throws IOException {
Objects.requireNonNull(key, "key must not be null");
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try {
writeLock.lock();
PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8);
try (Writer writer = Files.newBufferedWriter(path.resolve(percentEncoder.encode(key)))) {
writer.write(JsonUtil.toString(userProfile.asMap()));
}
} finally {
writeLock.unlock();
}
}
@Override
public void remove(String key) {
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try {
writeLock.lock();
PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8);
Files.deleteIfExists(path.resolve(percentEncoder.encode(key)));
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
writeLock.unlock();
}
}
@Override
public void purge(long expiredAfterSeconds) {
if (path != null && expiredAfterSeconds > 0L) {
Instant instant = Instant.now();
try (Stream<Path> stream = Files.walk(path)) {
stream.forEach(p -> {
try {
FileTime fileTime = Files.getLastModifiedTime(p);
Duration duration = Duration.between(fileTime.toInstant(), instant);
if (duration.toSeconds() > expiredAfterSeconds) {
Files.delete(p);
}
} catch (IOException e) {
logger.log(Level.WARNING, "i/o error while purge: " + e.getMessage(), e);
}
});
} catch (IOException e) {
logger.log(Level.WARNING, "i/o error while purge: " + e.getMessage(), e);
}
}
}
@Override
public void destroy() {
try (Stream<Path> stream = Files.walk(path)) {
stream.forEach(p -> {
try {
if (!path.equals(p)) {
Files.delete(p);
}
} catch (IOException e) {
logger.log(Level.WARNING, "i/o error while destroy: " + e.getMessage(), e);
}
});
} catch (IOException e) {
logger.log(Level.WARNING, "i/o error while destroy: " + e.getMessage(), e);
}
try {
Files.delete(path);
} catch (IOException e) {
logger.log(Level.WARNING, "i/o error while destroy: " + e.getMessage(), e);
}
}
}

View file

@ -0,0 +1,92 @@
package org.xbib.net.http.server.auth;
import org.xbib.net.UserProfile;
import org.xbib.net.http.server.persist.Codec;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MemoryPropertiesUserProfileCodec implements Codec<UserProfile> {
private static final Map<String, Object> store = new HashMap<>();
private final ReentrantReadWriteLock lock;
public MemoryPropertiesUserProfileCodec() {
this.lock = new ReentrantReadWriteLock();
}
@Override
public UserProfile create(String key) throws IOException {
BaseUserProfile baseUserProfile = new BaseUserProfile();
baseUserProfile.setUserId(key);
return baseUserProfile;
}
@Override
public UserProfile read(String key) throws IOException {
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
try {
readLock.lock();
Properties properties = new Properties();
if (store.containsKey(key)) {
properties.putAll((Map<?, ?>) store.get(key));
}
return toUserProfile(key, properties);
} finally {
readLock.unlock();
}
}
@Override
public void write(String key, UserProfile userProfile) throws IOException {
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try {
writeLock.lock();
Properties properties = toProperties(userProfile.asMap());
store.put(key, properties);
} finally {
writeLock.unlock();
}
}
private UserProfile toUserProfile(String key, Properties properties) {
Map<String, Object> map = new LinkedHashMap<>();
properties.forEach((k, v) -> map.put(k.toString(), v));
return BaseUserProfile.fromMap(map);
}
private Properties toProperties(Map<String, Object> map) {
Properties properties = new Properties();
map.forEach((k,v) -> {
// filter non-null keys and values for properties semantics
if (k != null && v != null) {
properties.put(k, v);
}
});
return properties;
}
@Override
public void remove(String key) throws IOException {
store.remove(key);
}
@Override
public void purge(long expiredAfterSeconds) throws IOException {
if (expiredAfterSeconds > 0L) {
for (Map.Entry<String, Object> entry : store.entrySet()) {
remove(entry.getKey());
}
}
}
@Override
public void destroy() {
store.clear();
}
}

View file

@ -10,8 +10,6 @@ import org.xbib.net.http.server.route.HttpRouterContext;
public class IncomingCookieHandler implements HttpHandler { public class IncomingCookieHandler implements HttpHandler {
private static final Logger logger = Logger.getLogger(IncomingCookieHandler.class.getName());
public IncomingCookieHandler() { public IncomingCookieHandler() {
} }
@ -30,7 +28,6 @@ public class IncomingCookieHandler implements HttpHandler {
} }
if (!cookieBox.isEmpty()) { if (!cookieBox.isEmpty()) {
context.getAttributes().put("incomingcookies", cookieBox); context.getAttributes().put("incomingcookies", cookieBox);
} }
} }
} }

View file

@ -13,4 +13,6 @@ public interface Codec<D> {
void remove(String key) throws IOException; void remove(String key) throws IOException;
void purge(long expiredAfterSeconds) throws IOException; void purge(long expiredAfterSeconds) throws IOException;
void destroy();
} }

View file

@ -67,6 +67,11 @@ public class FileJsonCodec implements Codec<Map<String, Object>> {
// unable to purge // unable to purge
} }
@Override
public void destroy() {
// unable to destroy
}
private Path openOrCreate(String key) throws IOException { private Path openOrCreate(String key) throws IOException {
Path path = Paths.get(root); Path path = Paths.get(root);
Files.createDirectories(path); Files.createDirectories(path);

View file

@ -7,10 +7,6 @@ import org.xbib.net.http.server.persist.Codec;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class FileJsonPersistenceStore extends AbstractPersistenceStore { public class FileJsonPersistenceStore extends AbstractPersistenceStore {
public FileJsonPersistenceStore(String name) {
this("/var/tmp/net-http-server-store", name);
}
public FileJsonPersistenceStore(String root, String storeName) { public FileJsonPersistenceStore(String root, String storeName) {
this(new FileJsonCodec(root), storeName); this(new FileJsonCodec(root), storeName);
} }

View file

@ -76,6 +76,11 @@ public class FilePropertiesCodec implements Codec<Map<String, Object>> {
// unable to purge // unable to purge
} }
@Override
public void destroy() {
// unable to destroy
}
private Path openOrCreate(String key) throws IOException { private Path openOrCreate(String key) throws IOException {
Path path = Paths.get(root); Path path = Paths.get(root);
Files.createDirectories(path); Files.createDirectories(path);

View file

@ -58,6 +58,11 @@ public class MemoryPropertiesCodec implements Codec<Map<String, Object>> {
// unable to purge // unable to purge
} }
@Override
public void destroy() {
store.clear();
}
private Map<String, Object> toMap(Properties properties) { private Map<String, Object> toMap(Properties properties) {
Map<String, Object> map = new LinkedHashMap<>(); Map<String, Object> map = new LinkedHashMap<>();
properties.forEach((k, v) -> map.put(k.toString(), v)); properties.forEach((k, v) -> map.put(k.toString(), v));

View file

@ -95,9 +95,7 @@ public class BaseHttpRouter implements HttpRouter {
requestBuilder.getRequestPath(), requestBuilder.getRequestPath(),
true); true);
builder.httpRouteResolver.resolve(httpRoute, httpRouteResolverResults::add); builder.httpRouteResolver.resolve(httpRoute, httpRouteResolverResults::add);
HttpRouterContext httpRouterContext = application.createContext(httpDomain, HttpRouterContext httpRouterContext = application.createContext(httpDomain, requestBuilder, responseBuilder);
requestBuilder, responseBuilder);
// before open: invoke security, incoming cookie/session
httpRouterContext.getOpenHandlers().forEach(h -> { httpRouterContext.getOpenHandlers().forEach(h -> {
try { try {
h.handle(httpRouterContext); h.handle(httpRouterContext);
@ -149,14 +147,10 @@ public class BaseHttpRouter implements HttpRouter {
setResolverResult(httpRouterContext, httpRouteResolverResult); setResolverResult(httpRouterContext, httpRouteResolverResult);
httpService = httpRouteResolverResult.getValue(); httpService = httpRouteResolverResult.getValue();
httpRequest = httpRouterContext.getRequest(); httpRequest = httpRouterContext.getRequest();
for (ApplicationModule module : application.getModules()) {
module.onOpen(httpRouterContext, httpService, httpRequest);
}
// second: security check, authentication etc. // second: security check, authentication etc.
if (httpService.getSecurityDomain() != null) { if (httpService.getSecurityDomain() != null) {
logger.log(Level.FINEST, "handling security domain service " + httpService);
for (HttpHandler httpHandler : httpService.getSecurityDomain().getHandlers()) { for (HttpHandler httpHandler : httpService.getSecurityDomain().getHandlers()) {
logger.log(Level.FINEST, () -> "handling security domain handler " + httpHandler); logger.log(Level.FINEST, () -> "handling security handler " + httpHandler);
httpHandler.handle(httpRouterContext); httpHandler.handle(httpRouterContext);
} }
} }
@ -164,6 +158,9 @@ public class BaseHttpRouter implements HttpRouter {
break; break;
} }
// after security checks, accept service, open and execute service // after security checks, accept service, open and execute service
for (ApplicationModule module : application.getModules()) {
module.onOpen(httpRouterContext, httpService, httpRequest);
}
httpRouterContext.getAttributes().put("service", httpService); httpRouterContext.getAttributes().put("service", httpService);
logger.log(Level.FINEST, "handling service " + httpService); logger.log(Level.FINEST, "handling service " + httpService);
httpService.handle(httpRouterContext); httpService.handle(httpRouterContext);

View file

@ -39,6 +39,8 @@ public class BaseHttpRouterContext implements HttpRouterContext {
private final Attributes attributes; private final Attributes attributes;
private final List<HttpHandler> securityHandlers;
private final List<HttpHandler> openHandlers; private final List<HttpHandler> openHandlers;
private final List<HttpHandler> closeHandlers; private final List<HttpHandler> closeHandlers;
@ -66,6 +68,7 @@ public class BaseHttpRouterContext implements HttpRouterContext {
this.application = application; this.application = application;
this.httpRequestBuilder = httpRequestBuilder; this.httpRequestBuilder = httpRequestBuilder;
this.httpResponseBuilder = httpResponseBuilder; this.httpResponseBuilder = httpResponseBuilder;
this.securityHandlers = new LinkedList<>();
this.openHandlers = new LinkedList<>(); this.openHandlers = new LinkedList<>();
this.closeHandlers = new LinkedList<>(); this.closeHandlers = new LinkedList<>();
this.releaseeHandlers = new LinkedList<>(); this.releaseeHandlers = new LinkedList<>();
@ -304,6 +307,12 @@ public class BaseHttpRouterContext implements HttpRouterContext {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
for (HttpHandler httpHandler : securityHandlers) {
if (httpHandler instanceof Closeable) {
logger.log(Level.FINE, "closing handler " + httpHandler);
((Closeable) httpHandler).close();
}
}
for (HttpHandler httpHandler : openHandlers) { for (HttpHandler httpHandler : openHandlers) {
if (httpHandler instanceof Closeable) { if (httpHandler instanceof Closeable) {
logger.log(Level.FINE, "closing handler " + httpHandler); logger.log(Level.FINE, "closing handler " + httpHandler);

View file

@ -1,4 +1,4 @@
package org.xbib.net.http.server.session.file; package org.xbib.net.http.server.session;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
@ -17,9 +17,6 @@ import java.util.stream.Stream;
import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders; import org.xbib.net.PercentEncoders;
import org.xbib.net.http.server.persist.Codec; import org.xbib.net.http.server.persist.Codec;
import org.xbib.net.http.server.session.BaseSession;
import org.xbib.net.http.server.session.Session;
import org.xbib.net.http.server.session.SessionListener;
import org.xbib.net.util.JsonUtil; import org.xbib.net.util.JsonUtil;
public class FileJsonSessionCodec implements Codec<Session> { public class FileJsonSessionCodec implements Codec<Session> {
@ -127,4 +124,26 @@ public class FileJsonSessionCodec implements Codec<Session> {
} }
} }
} }
@Override
public void destroy() {
try (Stream<Path> stream = Files.walk(path)) {
stream.forEach(p -> {
try {
if (!path.equals(p)) {
Files.delete(p);
}
} catch (IOException e) {
logger.log(Level.WARNING, "i/o error while destroy: " + e.getMessage(), e);
}
});
} catch (IOException e) {
logger.log(Level.WARNING, "i/o error while destroy: " + e.getMessage(), e);
}
try {
Files.delete(path);
} catch (IOException e) {
logger.log(Level.WARNING, "i/o error while destroy: " + e.getMessage(), e);
}
}
} }

View file

@ -18,16 +18,19 @@ import org.xbib.net.http.cookie.Cookie;
import org.xbib.net.http.cookie.CookieBox; import org.xbib.net.http.cookie.CookieBox;
import org.xbib.net.http.server.HttpException; import org.xbib.net.http.server.HttpException;
import org.xbib.net.http.server.HttpHandler; import org.xbib.net.http.server.HttpHandler;
import org.xbib.net.http.server.route.HttpRouterContext;
import org.xbib.net.http.server.auth.BaseUserProfile; import org.xbib.net.http.server.auth.BaseUserProfile;
import org.xbib.net.http.server.route.HttpRouterContext;
import org.xbib.net.http.server.cookie.CookieSignatureException; import org.xbib.net.http.server.cookie.CookieSignatureException;
import org.xbib.net.http.server.cookie.CookieSignatureUtil; import org.xbib.net.http.server.cookie.CookieSignatureUtil;
import org.xbib.net.http.server.persist.Codec; import org.xbib.net.http.server.persist.Codec;
import org.xbib.net.util.ExceptionFormatter;
public class IncomingContextHandler implements HttpHandler { public class IncomingContextHandler implements HttpHandler {
private static final Logger logger = Logger.getLogger(IncomingContextHandler.class.getName()); private static final Logger logger = Logger.getLogger(IncomingContextHandler.class.getName());
private final Codec<UserProfile> userProfileCodec;
private final String sessionSecret; private final String sessionSecret;
private final String sessionCookieAlgorithm; private final String sessionCookieAlgorithm;
@ -40,12 +43,14 @@ public class IncomingContextHandler implements HttpHandler {
Supplier<String> sessionIdGenerator; Supplier<String> sessionIdGenerator;
public IncomingContextHandler(String sessionSecret, public IncomingContextHandler(Codec<UserProfile> userProfileCodec,
String sessionSecret,
String sessionCookieAlgorithm, String sessionCookieAlgorithm,
String sessionCookieName, String sessionCookieName,
Codec<Session> sessionCodec, Codec<Session> sessionCodec,
Set<String> suffixes, Set<String> suffixes,
Supplier<String> sessionIdGenerator) { Supplier<String> sessionIdGenerator) {
this.userProfileCodec = userProfileCodec;
this.sessionSecret = sessionSecret; this.sessionSecret = sessionSecret;
this.sessionCookieAlgorithm = sessionCookieAlgorithm; this.sessionCookieAlgorithm = sessionCookieAlgorithm;
this.sessionCookieName = sessionCookieName; this.sessionCookieName = sessionCookieName;
@ -61,8 +66,8 @@ public class IncomingContextHandler implements HttpHandler {
return; return;
} }
Map<String, Object> payload = null; Map<String, Object> payload = null;
Session session = null;
UserProfile userProfile = null; UserProfile userProfile = null;
Session session = null;
CookieBox cookieBox = context.getAttributes().get(CookieBox.class, "incomingcookies"); CookieBox cookieBox = context.getAttributes().get(CookieBox.class, "incomingcookies");
if (cookieBox != null) { if (cookieBox != null) {
for (Cookie cookie : cookieBox) { for (Cookie cookie : cookieBox) {
@ -72,20 +77,21 @@ public class IncomingContextHandler implements HttpHandler {
try { try {
// extract payload from our cookie, must be not null, otherwise security problem with exception // extract payload from our cookie, must be not null, otherwise security problem with exception
payload = toPayload(cookie); payload = toPayload(cookie);
logger.log(Level.FINE, payload != null && !payload.isEmpty() ? "payload found" : "no payload");
// extract session from payload and recover session from persistence store // extract session from payload and recover session from persistence store
session = toSession(payload); session = toSession(payload);
// extract user profile from session
userProfile = toUserProfile(session);
// do not log explicit content of payload, session, userprofile to not leak sensitive info into log // do not log explicit content of payload, session, userprofile to not leak sensitive info into log
logger.log(Level.FINE, (payload != null && !payload.isEmpty() ? "payload found" : "no payload") + logger.log(Level.FINE, session != null && !session.isEmpty() ? "session found" : "no session");
(session != null && !session.isEmpty() ? ", session found" : ", no session") + // extract userprofile from cookie info, use previous auth handler setup, recover attributes only
(userProfile != null && userProfile.getUserId() != null ? ", user profile found (" + userProfile.getUserId() + ")" : ", no user profile")); userProfile = recoverUserProfile(context, session, payload);
logger.log(Level.FINE, userProfile != null ? "user profile found" : "no user profile");
} catch (CookieSignatureException e) { } catch (CookieSignatureException e) {
// set exception in context to discard broken cookie later and render exception message // set exception in context to discard broken cookie later and render exception message
context.getAttributes().put("_throwable", e); context.getAttributes().put("_throwable", e);
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, e.getMessage(), e); logger.log(Level.SEVERE, e.getMessage(), e);
throw new HttpException("unable to create session", context, HttpResponseStatus.INTERNAL_SERVER_ERROR); throw new HttpException("unable to create userprofile or session: " + ExceptionFormatter.format(e),
context, HttpResponseStatus.INTERNAL_SERVER_ERROR);
} }
} else { } else {
logger.log(Level.WARNING, "received extra session cookie of same name, something is wrong, ignoring"); logger.log(Level.WARNING, "received extra session cookie of same name, something is wrong, ignoring");
@ -94,13 +100,21 @@ public class IncomingContextHandler implements HttpHandler {
} }
} }
if (session == null) { if (session == null) {
session = newSession(context); session = createSession(context);
if (session != null) {
logger.log(Level.FINE, "new session created, id = " + session.id()); logger.log(Level.FINE, "new session created, id = " + session.id());
} }
}
context.getAttributes().put("session", session); context.getAttributes().put("session", session);
if (userProfile == null) { if (userProfile == null) {
userProfile = newUserProfile(payload); try {
logger.log(Level.FINE, "new user profile created"); userProfile = recoverUserProfile(context, session, payload);
if (userProfile != null) {
logger.log(Level.FINE, "new user profile recovered");
}
} catch (IOException e) {
logger.log(Level.FINE, "unable to recover new user profile: " + e.getMessage(), e);
}
} }
context.getAttributes().put("userprofile", userProfile); context.getAttributes().put("userprofile", userProfile);
} }
@ -127,11 +141,11 @@ public class IncomingContextHandler implements HttpHandler {
return Map.of("id", id, "payload", payload, "map", CookieSignatureUtil.toMap(payload)); return Map.of("id", id, "payload", payload, "map", CookieSignatureUtil.toMap(payload));
} }
protected Session toSession(Map<String, Object> map) { protected Session toSession(Map<String, Object> payload) {
if (map == null) { if (payload == null) {
return null; return null;
} }
String id = (String) map.get("id"); String id = (String) payload.get("id");
Session session = null; Session session = null;
try { try {
if (id != null) { if (id != null) {
@ -139,7 +153,7 @@ public class IncomingContextHandler implements HttpHandler {
if (session != null) { if (session != null) {
logger.log(Level.FINE, "session id " + id + " restored, valid = " + session.isValid() + logger.log(Level.FINE, "session id " + id + " restored, valid = " + session.isValid() +
", age = " + session.getAge() + ", expired = " + session.isExpired()); ", age = " + session.getAge() + ", expired = " + session.isExpired());
session.putAll(map); session.putAll(payload);
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -148,7 +162,7 @@ public class IncomingContextHandler implements HttpHandler {
return session; return session;
} }
protected Session newSession(HttpRouterContext context) throws HttpException { protected Session createSession(HttpRouterContext context) throws HttpException {
try { try {
return sessionCodec.create(sessionIdGenerator.get()); return sessionCodec.create(sessionIdGenerator.get());
} catch (IOException e) { } catch (IOException e) {
@ -158,32 +172,44 @@ public class IncomingContextHandler implements HttpHandler {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected UserProfile toUserProfile(Session session) { protected UserProfile recoverUserProfile(HttpRouterContext context,
if (session == null) { Session session,
return null; Map<String, Object> payload)
} throws IOException {
if (payload == null && session != null) {
// session has user profile? this is important for spanning HTTP request/response like in HTTP POST/FORWARD/GET
Map<String, Object> map = (Map<String, Object>) session.get("userprofile"); Map<String, Object> map = (Map<String, Object>) session.get("userprofile");
return map != null ? BaseUserProfile.fromMap(map) : null; if (map != null) {
UserProfile userProfile = BaseUserProfile.fromMap(map);
if (userProfile.getAttributes().isEmpty()) {
// recover complete user profile from codec
userProfileCodec.read(userProfile.getUserId());
} }
@SuppressWarnings("unchecked")
protected UserProfile newUserProfile(Map<String, Object> cookieMap) {
UserProfile userProfile = new BaseUserProfile();
// user_id, e_user_id are in cookie map
if (cookieMap != null) {
Map<String, Object> m = (Map<String, Object>) cookieMap.get("map");
if (m == null) {
// nothing found
return userProfile; return userProfile;
} }
if (m.containsKey("user_id")) {
userProfile.setUserId((String) m.get("user_id"));
} }
if (payload != null) {
// from cookie
Map<String, Object> m = (Map<String, Object>) payload.get("map");
if (m != null && m.containsKey("user_id")) {
String key = (String) m.get("user_id");
logger.log(Level.INFO, "recover: user id = " + key);
// set by previous handler?
UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile");
if (userProfile == null) {
userProfile = new BaseUserProfile();
}
userProfile.setUserId(key);
if (m.containsKey("e_user_id")) { if (m.containsKey("e_user_id")) {
userProfile.setEffectiveUserId((String) m.get("e_user_id")); userProfile.setEffectiveUserId((String) m.get("e_user_id"));
} }
// roles, permissions, attributes must be restored later if (userProfile.getAttributes().isEmpty()) {
// recover complete user profile from codec
userProfileCodec.read(key);
} }
return userProfile; return userProfile;
} }
} }
return null;
}
}

View file

@ -1,4 +1,4 @@
package org.xbib.net.http.server.session.jdbc; package org.xbib.net.http.server.session;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
@ -14,9 +14,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import javax.sql.DataSource; import javax.sql.DataSource;
import org.xbib.net.http.server.persist.Codec; import org.xbib.net.http.server.persist.Codec;
import org.xbib.net.http.server.session.BaseSession;
import org.xbib.net.http.server.session.Session;
import org.xbib.net.http.server.session.SessionListener;
import org.xbib.net.util.JsonUtil; import org.xbib.net.util.JsonUtil;
public class JdbcSessionCodec implements Codec<Session> { public class JdbcSessionCodec implements Codec<Session> {
@ -132,6 +129,11 @@ public class JdbcSessionCodec implements Codec<Session> {
} }
} }
@Override
public void destroy() {
// TODO
}
private String readString(String key) throws SQLException { private String readString(String key) throws SQLException {
List<String> list = new ArrayList<>(); List<String> list = new ArrayList<>();
Connection connection = dataSource.getConnection(); Connection connection = dataSource.getConnection();

View file

@ -1,4 +1,4 @@
package org.xbib.net.http.server.session.memory; package org.xbib.net.http.server.session;
import java.io.IOException; import java.io.IOException;
import java.time.Duration; import java.time.Duration;
@ -7,9 +7,6 @@ import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.xbib.net.http.server.persist.Codec; import org.xbib.net.http.server.persist.Codec;
import org.xbib.net.http.server.session.BaseSession;
import org.xbib.net.http.server.session.Session;
import org.xbib.net.http.server.session.SessionListener;
public class MemoryPropertiesSessionCodec implements Codec<Session> { public class MemoryPropertiesSessionCodec implements Codec<Session> {
@ -101,4 +98,10 @@ public class MemoryPropertiesSessionCodec implements Codec<Session> {
} }
} }
} }
@Override
public void destroy() {
store.clear();
}
} }

View file

@ -5,10 +5,11 @@ import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Duration; import java.time.Duration;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.xbib.datastructures.tiny.TinyMap;
import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders; import org.xbib.net.PercentEncoders;
import org.xbib.net.UserProfile; import org.xbib.net.UserProfile;
@ -85,10 +86,10 @@ public class OutgoingContextHandler implements HttpHandler {
cookieBox.add(createEmptyCookie(host, path)); cookieBox.add(createEmptyCookie(host, path));
return; return;
} }
UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile");
Session session = context.getAttributes().get(Session.class, "session"); Session session = context.getAttributes().get(Session.class, "session");
if (session != null) { if (session != null) {
try { try {
UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile");
if (userProfile != null) { if (userProfile != null) {
session.put("userprofile", userProfile.asMap()); session.put("userprofile", userProfile.asMap());
} }
@ -127,11 +128,13 @@ public class OutgoingContextHandler implements HttpHandler {
session.invalidate(); session.invalidate();
return createEmptyCookie(host, path); return createEmptyCookie(host, path);
} }
Map<String, Object> map = userProfile != null ? TinyMap.Builder<String, Object> builder = TinyMap.builder();
Map.of("user_id", userProfile.getUserId() != null ? userProfile.getUserId() :"", if (userProfile != null) {
"e_user_id", userProfile.getEffectiveUserId() != null ? userProfile.getEffectiveUserId() : "") : builder.putIfNotNull("user_id", userProfile.getUserId());
Map.of(); builder.putIfNotNull("e_user_id", userProfile.getEffectiveUserId());
String payload = CookieSignatureUtil.toString(map); }
logger.log(Level.FINEST, "map for cookie payload = " + builder.build());
String payload = CookieSignatureUtil.toString(builder.build());
String sig = CookieSignatureUtil.hmac(payload, sessionSecret, sessionCookieAlgorithm); String sig = CookieSignatureUtil.hmac(payload, sessionSecret, sessionCookieAlgorithm);
String cookieValue = String.join(":", id, payload, sig); String cookieValue = String.join(":", id, payload, sig);
PercentEncoder percentEncoder = PercentEncoders.getCookieEncoder(StandardCharsets.ISO_8859_1); PercentEncoder percentEncoder = PercentEncoders.getCookieEncoder(StandardCharsets.ISO_8859_1);

View file

@ -3,6 +3,8 @@ package org.xbib.net.http.server.session;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.xbib.net.UserProfile;
import org.xbib.net.http.HttpResponseStatus; import org.xbib.net.http.HttpResponseStatus;
import org.xbib.net.http.server.HttpException; import org.xbib.net.http.server.HttpException;
import org.xbib.net.http.server.HttpHandler; import org.xbib.net.http.server.HttpHandler;
@ -13,14 +15,31 @@ public class PersistSessionHandler implements HttpHandler {
private static final Logger logger = Logger.getLogger(PersistSessionHandler.class.getName()); private static final Logger logger = Logger.getLogger(PersistSessionHandler.class.getName());
private final Codec<UserProfile> userProfileCodec;
private final Codec<Session> sessionCodec; private final Codec<Session> sessionCodec;
public PersistSessionHandler(Codec<Session> sessionCodec) { public PersistSessionHandler(Codec<UserProfile> userProfileCodec,
Codec<Session> sessionCodec) {
this.userProfileCodec = userProfileCodec;
this.sessionCodec = sessionCodec; this.sessionCodec = sessionCodec;
} }
@Override @Override
public void handle(HttpRouterContext context) throws IOException { public void handle(HttpRouterContext context) throws IOException {
UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile");
if (userProfile != null && userProfile.getUserId() != null) {
try {
logger.log(Level.FINEST, "writing user profile id " + userProfile.getUserId());
userProfileCodec.write(userProfile.getUserId(), userProfile);
} catch (Exception e) {
logger.log(Level.SEVERE, e.getMessage(), e);
throw new HttpException("unable to write user profile data", context, HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
} else {
logger.log(Level.FINEST, "not writing user profile " + userProfile + " user id + " +
(userProfile != null ? userProfile.getUserId() : null));
}
Session session = context.getAttributes().get(Session.class, "session"); Session session = context.getAttributes().get(Session.class, "session");
if (session != null) { if (session != null) {
try { try {

View file

@ -6,7 +6,9 @@ module org.xbib.net.http.server.test {
exports org.xbib.net.http.server.test.base; exports org.xbib.net.http.server.test.base;
exports org.xbib.net.http.server.test.ldap; exports org.xbib.net.http.server.test.ldap;
exports org.xbib.net.http.server.test.session; exports org.xbib.net.http.server.test.session;
exports org.xbib.net.http.server.test.userprofile;
opens org.xbib.net.http.server.test.base; opens org.xbib.net.http.server.test.base;
opens org.xbib.net.http.server.test.ldap; opens org.xbib.net.http.server.test.ldap;
opens org.xbib.net.http.server.test.session; opens org.xbib.net.http.server.test.session;
opens org.xbib.net.http.server.test.userprofile;
} }

View file

@ -4,17 +4,16 @@ import java.io.IOException;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.Duration; import java.time.Duration;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.xbib.net.http.server.persist.Codec; import org.xbib.net.http.server.persist.Codec;
import org.xbib.net.http.server.session.Session; import org.xbib.net.http.server.session.Session;
import org.xbib.net.http.server.session.file.FileJsonSessionCodec; import org.xbib.net.http.server.session.FileJsonSessionCodec;
import org.xbib.net.util.RandomUtil; import org.xbib.net.util.RandomUtil;
public class JsonSessionTest { import static org.junit.jupiter.api.Assertions.assertEquals;
private static final Logger logger = Logger.getLogger(JsonSessionTest.class.getName()); public class JsonSessionTest {
@Test @Test
void testJsonSession() throws IOException { void testJsonSession() throws IOException {
@ -23,12 +22,13 @@ public class JsonSessionTest {
session.put("a", "b"); session.put("a", "b");
sessionCodec.write(session.id(), session); sessionCodec.write(session.id(), session);
Session session1 = sessionCodec.read(session.id()); Session session1 = sessionCodec.read(session.id());
logger.log(Level.INFO, session1.get("a").toString()); assertEquals("b", session1.get("a").toString());
sessionCodec.destroy();
} }
private Codec<Session> newSessionCodec() { private Codec<Session> newSessionCodec() {
return new FileJsonSessionCodec("SESSION-TEST", null, 1024, return new FileJsonSessionCodec("SESSION-TEST", null, 1024,
Duration.ofDays(1), Paths.get("/var/tmp/session-test")); Duration.ofDays(1), Paths.get("build/session-test"));
} }
private Session create(Codec<Session> sessionCodec, Supplier<String> sessionIdGenerator) throws IOException { private Session create(Codec<Session> sessionCodec, Supplier<String> sessionIdGenerator) throws IOException {

View file

@ -0,0 +1,34 @@
package org.xbib.net.http.server.test.userprofile;
import org.junit.jupiter.api.Test;
import org.xbib.net.UserProfile;
import org.xbib.net.http.server.persist.Codec;
import org.xbib.net.http.server.auth.FileJsonUserProfileCodec;
import java.io.IOException;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class JsonUserProfileTest {
@Test
void testJsonUserProfile() throws IOException {
Codec<UserProfile> userProfileCodec = newUserProfileCodec();
UserProfile userProfile = create(userProfileCodec);
userProfile.getAttributes().put("a", "b");
userProfileCodec.write(userProfile.getUserId(), userProfile);
UserProfile userProfile1 = userProfileCodec.read(userProfile.getUserId());
assertEquals("b", userProfile1.getAttributes().get("a").toString());
userProfileCodec.destroy();
}
private Codec<UserProfile> newUserProfileCodec() {
return new FileJsonUserProfileCodec(Paths.get("build/userprofile-test"));
}
private UserProfile create(Codec<UserProfile> userProfileCodec) throws IOException {
return userProfileCodec.create("1");
}
}