From 1ad8f686dfd2af4acc92d4f7d214f09b7ce1b422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Prante?= Date: Mon, 3 Jun 2024 19:13:29 +0200 Subject: [PATCH] add user profile persistence, for adding user preferences --- gradle.properties | 2 +- .../application/web/WebApplication.java | 9 +- .../src/main/java/module-info.java | 2 - .../server/application/BaseApplication.java | 28 ++-- .../server/auth/FileJsonUserProfileCodec.java | 139 ++++++++++++++++++ .../MemoryPropertiesUserProfileCodec.java | 92 ++++++++++++ .../server/cookie/IncomingCookieHandler.java | 3 - .../xbib/net/http/server/persist/Codec.java | 2 + .../server/persist/file/FileJsonCodec.java | 5 + .../file/FileJsonPersistenceStore.java | 4 - .../persist/file/FilePropertiesCodec.java | 5 + .../persist/memory/MemoryPropertiesCodec.java | 5 + .../net/http/server/route/BaseHttpRouter.java | 13 +- .../server/route/BaseHttpRouterContext.java | 9 ++ .../{file => }/FileJsonSessionCodec.java | 27 +++- .../session/IncomingContextHandler.java | 110 ++++++++------ .../session/{jdbc => }/JdbcSessionCodec.java | 10 +- .../MemoryPropertiesSessionCodec.java | 11 +- .../session/OutgoingContextHandler.java | 17 ++- .../server/session/PersistSessionHandler.java | 21 ++- .../src/test/java/module-info.java | 2 + .../server/test/session/JsonSessionTest.java | 14 +- .../test/userprofile/JsonUserProfileTest.java | 34 +++++ 23 files changed, 466 insertions(+), 98 deletions(-) create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/auth/FileJsonUserProfileCodec.java create mode 100644 net-http-server/src/main/java/org/xbib/net/http/server/auth/MemoryPropertiesUserProfileCodec.java rename net-http-server/src/main/java/org/xbib/net/http/server/session/{file => }/FileJsonSessionCodec.java (85%) rename net-http-server/src/main/java/org/xbib/net/http/server/session/{jdbc => }/JdbcSessionCodec.java (96%) rename net-http-server/src/main/java/org/xbib/net/http/server/session/{memory => }/MemoryPropertiesSessionCodec.java (94%) create mode 100644 net-http-server/src/test/java/org/xbib/net/http/server/test/userprofile/JsonUserProfileTest.java diff --git a/gradle.properties b/gradle.properties index 9cded4d..d78ccfb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ group = org.xbib name = net-http -version = 4.6.0 +version = 4.7.0 diff --git a/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java b/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java index 1711df0..6ba2bc2 100644 --- a/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java +++ b/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java @@ -3,11 +3,13 @@ package org.xbib.net.http.server.application.web; import java.nio.file.Paths; import java.time.Duration; +import org.xbib.net.UserProfile; import org.xbib.net.http.server.application.BaseApplication; import org.xbib.net.http.server.route.HttpRouterContext; import org.xbib.net.http.server.persist.Codec; 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 { @@ -28,4 +30,9 @@ public class WebApplication extends BaseApplication { return new FileJsonSessionCodec(sessionName, this, 1024, Duration.ofDays(1), Paths.get("/var/tmp/session")); } + + @Override + protected Codec newUserProfileCodec(HttpRouterContext httpRouterContext) { + return new FileJsonUserProfileCodec(Paths.get("/var/tmp/userprofile")); + } } diff --git a/net-http-server/src/main/java/module-info.java b/net-http-server/src/main/java/module-info.java index 29c38fe..7fc9586 100644 --- a/net-http-server/src/main/java/module-info.java +++ b/net-http-server/src/main/java/module-info.java @@ -27,8 +27,6 @@ module org.xbib.net.http.server { exports org.xbib.net.http.server.route; exports org.xbib.net.http.server.service; 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.executor; } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java b/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java index 33ef9fc..9f779c0 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java @@ -15,6 +15,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.xbib.net.Attributes; +import org.xbib.net.UserProfile; import org.xbib.net.http.HttpAddress; import org.xbib.net.http.cookie.SameSite; 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.PersistSessionHandler; 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.mime.MimeTypeService; import org.xbib.net.util.RandomUtil; @@ -168,13 +170,15 @@ public class BaseApplication implements Application { HttpRouterContext httpRouterContext = new BaseHttpRouterContext(this, domain, requestBuilder, responseBuilder); httpRouterContext.addOpenHandler(newRequestValidator()); httpRouterContext.addOpenHandler(newIncomingCookieHandler()); - if (builder.sessionsEnabled) { - Codec sessionCodec = newSessionCodec(httpRouterContext); + Codec userProfileCodec = newUserProfileCodec(httpRouterContext); + httpRouterContext.getAttributes().put("userprofilecodec", userProfileCodec); + Codec sessionCodec = builder.sessionsEnabled ? newSessionCodec(httpRouterContext) : null; + if (sessionCodec != null) { 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()); return httpRouterContext; } @@ -212,8 +216,12 @@ public class BaseApplication implements Application { return new MemoryPropertiesSessionCodec(sessionName,this, 1024, Duration.ofDays(1)); } - protected HttpHandler newIncomingContextHandler(Codec sessionCodec) { - return new IncomingContextHandler( + protected Codec newUserProfileCodec(HttpRouterContext httpRouterContext) { + return new MemoryPropertiesUserProfileCodec(); + } + + protected HttpHandler newIncomingContextHandler(Codec userProfileCodec, Codec sessionCodec) { + return new IncomingContextHandler(userProfileCodec, getSecret(), "HmacSHA1", sessionName, @@ -235,8 +243,8 @@ public class BaseApplication implements Application { ); } - protected HttpHandler newPersistHandler(Codec sessionCodec) { - return new PersistSessionHandler(sessionCodec); + protected HttpHandler newPersistHandler(Codec userProfileCodec, Codec sessionCodec) { + return new PersistSessionHandler(userProfileCodec, sessionCodec); } @Override diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/auth/FileJsonUserProfileCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/auth/FileJsonUserProfileCodec.java new file mode 100644 index 0000000..0f1270b --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/auth/FileJsonUserProfileCodec.java @@ -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 { + + 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 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 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 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); + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/auth/MemoryPropertiesUserProfileCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/auth/MemoryPropertiesUserProfileCodec.java new file mode 100644 index 0000000..6492430 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/auth/MemoryPropertiesUserProfileCodec.java @@ -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 { + + private static final Map 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 map = new LinkedHashMap<>(); + properties.forEach((k, v) -> map.put(k.toString(), v)); + return BaseUserProfile.fromMap(map); + } + + private Properties toProperties(Map 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 entry : store.entrySet()) { + remove(entry.getKey()); + } + } + } + + @Override + public void destroy() { + store.clear(); + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/cookie/IncomingCookieHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/IncomingCookieHandler.java index e974556..fd13f4a 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/cookie/IncomingCookieHandler.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/cookie/IncomingCookieHandler.java @@ -10,8 +10,6 @@ import org.xbib.net.http.server.route.HttpRouterContext; public class IncomingCookieHandler implements HttpHandler { - private static final Logger logger = Logger.getLogger(IncomingCookieHandler.class.getName()); - public IncomingCookieHandler() { } @@ -30,7 +28,6 @@ public class IncomingCookieHandler implements HttpHandler { } if (!cookieBox.isEmpty()) { context.getAttributes().put("incomingcookies", cookieBox); - } } } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/Codec.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/Codec.java index 4f52bb6..0705534 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/persist/Codec.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/Codec.java @@ -13,4 +13,6 @@ public interface Codec { void remove(String key) throws IOException; void purge(long expiredAfterSeconds) throws IOException; + + void destroy(); } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonCodec.java index dc3becf..dff3ff8 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonCodec.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonCodec.java @@ -67,6 +67,11 @@ public class FileJsonCodec implements Codec> { // unable to purge } + @Override + public void destroy() { + // unable to destroy + } + private Path openOrCreate(String key) throws IOException { Path path = Paths.get(root); Files.createDirectories(path); diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonPersistenceStore.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonPersistenceStore.java index 9698b3a..07c666f 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonPersistenceStore.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FileJsonPersistenceStore.java @@ -7,10 +7,6 @@ import org.xbib.net.http.server.persist.Codec; @SuppressWarnings("serial") public class FileJsonPersistenceStore extends AbstractPersistenceStore { - public FileJsonPersistenceStore(String name) { - this("/var/tmp/net-http-server-store", name); - } - public FileJsonPersistenceStore(String root, String storeName) { this(new FileJsonCodec(root), storeName); } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FilePropertiesCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FilePropertiesCodec.java index 0dbac69..c97b6d9 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FilePropertiesCodec.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/file/FilePropertiesCodec.java @@ -76,6 +76,11 @@ public class FilePropertiesCodec implements Codec> { // unable to purge } + @Override + public void destroy() { + // unable to destroy + } + private Path openOrCreate(String key) throws IOException { Path path = Paths.get(root); Files.createDirectories(path); diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/persist/memory/MemoryPropertiesCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/persist/memory/MemoryPropertiesCodec.java index 0c188ab..ca1602f 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/persist/memory/MemoryPropertiesCodec.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/persist/memory/MemoryPropertiesCodec.java @@ -58,6 +58,11 @@ public class MemoryPropertiesCodec implements Codec> { // unable to purge } + @Override + public void destroy() { + store.clear(); + } + private Map toMap(Properties properties) { Map map = new LinkedHashMap<>(); properties.forEach((k, v) -> map.put(k.toString(), v)); diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java index de4e10a..dba78b5 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java @@ -95,9 +95,7 @@ public class BaseHttpRouter implements HttpRouter { requestBuilder.getRequestPath(), true); builder.httpRouteResolver.resolve(httpRoute, httpRouteResolverResults::add); - HttpRouterContext httpRouterContext = application.createContext(httpDomain, - requestBuilder, responseBuilder); - // before open: invoke security, incoming cookie/session + HttpRouterContext httpRouterContext = application.createContext(httpDomain, requestBuilder, responseBuilder); httpRouterContext.getOpenHandlers().forEach(h -> { try { h.handle(httpRouterContext); @@ -149,14 +147,10 @@ public class BaseHttpRouter implements HttpRouter { setResolverResult(httpRouterContext, httpRouteResolverResult); httpService = httpRouteResolverResult.getValue(); httpRequest = httpRouterContext.getRequest(); - for (ApplicationModule module : application.getModules()) { - module.onOpen(httpRouterContext, httpService, httpRequest); - } // second: security check, authentication etc. if (httpService.getSecurityDomain() != null) { - logger.log(Level.FINEST, "handling security domain service " + httpService); 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); } } @@ -164,6 +158,9 @@ public class BaseHttpRouter implements HttpRouter { break; } // after security checks, accept service, open and execute service + for (ApplicationModule module : application.getModules()) { + module.onOpen(httpRouterContext, httpService, httpRequest); + } httpRouterContext.getAttributes().put("service", httpService); logger.log(Level.FINEST, "handling service " + httpService); httpService.handle(httpRouterContext); diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterContext.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterContext.java index a079e4f..8eb5ae6 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterContext.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouterContext.java @@ -39,6 +39,8 @@ public class BaseHttpRouterContext implements HttpRouterContext { private final Attributes attributes; + private final List securityHandlers; + private final List openHandlers; private final List closeHandlers; @@ -66,6 +68,7 @@ public class BaseHttpRouterContext implements HttpRouterContext { this.application = application; this.httpRequestBuilder = httpRequestBuilder; this.httpResponseBuilder = httpResponseBuilder; + this.securityHandlers = new LinkedList<>(); this.openHandlers = new LinkedList<>(); this.closeHandlers = new LinkedList<>(); this.releaseeHandlers = new LinkedList<>(); @@ -304,6 +307,12 @@ public class BaseHttpRouterContext implements HttpRouterContext { @Override 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) { if (httpHandler instanceof Closeable) { logger.log(Level.FINE, "closing handler " + httpHandler); diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/file/FileJsonSessionCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/FileJsonSessionCodec.java similarity index 85% rename from net-http-server/src/main/java/org/xbib/net/http/server/session/file/FileJsonSessionCodec.java rename to net-http-server/src/main/java/org/xbib/net/http/server/session/FileJsonSessionCodec.java index 06b222e..3f864b2 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/file/FileJsonSessionCodec.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/FileJsonSessionCodec.java @@ -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.UncheckedIOException; @@ -17,9 +17,6 @@ import java.util.stream.Stream; import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoders; 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; public class FileJsonSessionCodec implements Codec { @@ -127,4 +124,26 @@ public class FileJsonSessionCodec implements Codec { } } } + + @Override + public void destroy() { + try (Stream 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); + } + } } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingContextHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingContextHandler.java index 51cef05..7fee936 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingContextHandler.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingContextHandler.java @@ -18,16 +18,19 @@ import org.xbib.net.http.cookie.Cookie; import org.xbib.net.http.cookie.CookieBox; import org.xbib.net.http.server.HttpException; 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.route.HttpRouterContext; import org.xbib.net.http.server.cookie.CookieSignatureException; import org.xbib.net.http.server.cookie.CookieSignatureUtil; import org.xbib.net.http.server.persist.Codec; +import org.xbib.net.util.ExceptionFormatter; public class IncomingContextHandler implements HttpHandler { private static final Logger logger = Logger.getLogger(IncomingContextHandler.class.getName()); + private final Codec userProfileCodec; + private final String sessionSecret; private final String sessionCookieAlgorithm; @@ -40,12 +43,14 @@ public class IncomingContextHandler implements HttpHandler { Supplier sessionIdGenerator; - public IncomingContextHandler(String sessionSecret, + public IncomingContextHandler(Codec userProfileCodec, + String sessionSecret, String sessionCookieAlgorithm, String sessionCookieName, Codec sessionCodec, Set suffixes, Supplier sessionIdGenerator) { + this.userProfileCodec = userProfileCodec; this.sessionSecret = sessionSecret; this.sessionCookieAlgorithm = sessionCookieAlgorithm; this.sessionCookieName = sessionCookieName; @@ -61,8 +66,8 @@ public class IncomingContextHandler implements HttpHandler { return; } Map payload = null; - Session session = null; UserProfile userProfile = null; + Session session = null; CookieBox cookieBox = context.getAttributes().get(CookieBox.class, "incomingcookies"); if (cookieBox != null) { for (Cookie cookie : cookieBox) { @@ -72,20 +77,21 @@ public class IncomingContextHandler implements HttpHandler { try { // extract payload from our cookie, must be not null, otherwise security problem with exception 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 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 - logger.log(Level.FINE, (payload != null && !payload.isEmpty() ? "payload found" : "no payload") + - (session != null && !session.isEmpty() ? ", session found" : ", no session") + - (userProfile != null && userProfile.getUserId() != null ? ", user profile found (" + userProfile.getUserId() + ")" : ", no user profile")); + logger.log(Level.FINE, session != null && !session.isEmpty() ? "session found" : "no session"); + // extract userprofile from cookie info, use previous auth handler setup, recover attributes only + userProfile = recoverUserProfile(context, session, payload); + logger.log(Level.FINE, userProfile != null ? "user profile found" : "no user profile"); } catch (CookieSignatureException e) { // set exception in context to discard broken cookie later and render exception message context.getAttributes().put("_throwable", e); } catch (Exception 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 { 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) { - session = newSession(context); - logger.log(Level.FINE, "new session created, id = " + session.id()); + session = createSession(context); + if (session != null) { + logger.log(Level.FINE, "new session created, id = " + session.id()); + } } context.getAttributes().put("session", session); if (userProfile == null) { - userProfile = newUserProfile(payload); - logger.log(Level.FINE, "new user profile created"); + try { + 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); } @@ -127,11 +141,11 @@ public class IncomingContextHandler implements HttpHandler { return Map.of("id", id, "payload", payload, "map", CookieSignatureUtil.toMap(payload)); } - protected Session toSession(Map map) { - if (map == null) { + protected Session toSession(Map payload) { + if (payload == null) { return null; } - String id = (String) map.get("id"); + String id = (String) payload.get("id"); Session session = null; try { if (id != null) { @@ -139,7 +153,7 @@ public class IncomingContextHandler implements HttpHandler { if (session != null) { logger.log(Level.FINE, "session id " + id + " restored, valid = " + session.isValid() + ", age = " + session.getAge() + ", expired = " + session.isExpired()); - session.putAll(map); + session.putAll(payload); } } } catch (Exception e) { @@ -148,7 +162,7 @@ public class IncomingContextHandler implements HttpHandler { return session; } - protected Session newSession(HttpRouterContext context) throws HttpException { + protected Session createSession(HttpRouterContext context) throws HttpException { try { return sessionCodec.create(sessionIdGenerator.get()); } catch (IOException e) { @@ -158,32 +172,44 @@ public class IncomingContextHandler implements HttpHandler { } @SuppressWarnings("unchecked") - protected UserProfile toUserProfile(Session session) { - if (session == null) { - return null; - } - Map map = (Map) session.get("userprofile"); - return map != null ? BaseUserProfile.fromMap(map) : null; - } - - @SuppressWarnings("unchecked") - protected UserProfile newUserProfile(Map cookieMap) { - UserProfile userProfile = new BaseUserProfile(); - // user_id, e_user_id are in cookie map - if (cookieMap != null) { - Map m = (Map) cookieMap.get("map"); - if (m == null) { - // nothing found + protected UserProfile recoverUserProfile(HttpRouterContext context, + Session session, + Map 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 map = (Map) session.get("userprofile"); + if (map != null) { + UserProfile userProfile = BaseUserProfile.fromMap(map); + if (userProfile.getAttributes().isEmpty()) { + // recover complete user profile from codec + userProfileCodec.read(userProfile.getUserId()); + } return userProfile; } - if (m.containsKey("user_id")) { - userProfile.setUserId((String) m.get("user_id")); - } - if (m.containsKey("e_user_id")) { - userProfile.setEffectiveUserId((String) m.get("e_user_id")); - } - // roles, permissions, attributes must be restored later } - return userProfile; + if (payload != null) { + // from cookie + Map m = (Map) 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")) { + userProfile.setEffectiveUserId((String) m.get("e_user_id")); + } + if (userProfile.getAttributes().isEmpty()) { + // recover complete user profile from codec + userProfileCodec.read(key); + } + return userProfile; + } + } + return null; } } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/jdbc/JdbcSessionCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/JdbcSessionCodec.java similarity index 96% rename from net-http-server/src/main/java/org/xbib/net/http/server/session/jdbc/JdbcSessionCodec.java rename to net-http-server/src/main/java/org/xbib/net/http/server/session/JdbcSessionCodec.java index c2ddd56..df9976c 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/jdbc/JdbcSessionCodec.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/JdbcSessionCodec.java @@ -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.UncheckedIOException; @@ -14,9 +14,6 @@ import java.util.List; import java.util.Map; import javax.sql.DataSource; 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; public class JdbcSessionCodec implements Codec { @@ -132,6 +129,11 @@ public class JdbcSessionCodec implements Codec { } } + @Override + public void destroy() { + // TODO + } + private String readString(String key) throws SQLException { List list = new ArrayList<>(); Connection connection = dataSource.getConnection(); diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/memory/MemoryPropertiesSessionCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/MemoryPropertiesSessionCodec.java similarity index 94% rename from net-http-server/src/main/java/org/xbib/net/http/server/session/memory/MemoryPropertiesSessionCodec.java rename to net-http-server/src/main/java/org/xbib/net/http/server/session/MemoryPropertiesSessionCodec.java index cef7b16..fe31e0d 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/memory/MemoryPropertiesSessionCodec.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/MemoryPropertiesSessionCodec.java @@ -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.time.Duration; @@ -7,9 +7,6 @@ import java.util.Map; import java.util.Properties; import java.util.concurrent.locks.ReentrantReadWriteLock; 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 { @@ -101,4 +98,10 @@ public class MemoryPropertiesSessionCodec implements Codec { } } } + + @Override + public void destroy() { + store.clear(); + + } } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingContextHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingContextHandler.java index b9c8306..413690f 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingContextHandler.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingContextHandler.java @@ -5,10 +5,11 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.Duration; -import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; + +import org.xbib.datastructures.tiny.TinyMap; import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoders; import org.xbib.net.UserProfile; @@ -85,10 +86,10 @@ public class OutgoingContextHandler implements HttpHandler { cookieBox.add(createEmptyCookie(host, path)); return; } + UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile"); Session session = context.getAttributes().get(Session.class, "session"); if (session != null) { try { - UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile"); if (userProfile != null) { session.put("userprofile", userProfile.asMap()); } @@ -127,11 +128,13 @@ public class OutgoingContextHandler implements HttpHandler { session.invalidate(); return createEmptyCookie(host, path); } - Map map = userProfile != null ? - Map.of("user_id", userProfile.getUserId() != null ? userProfile.getUserId() :"", - "e_user_id", userProfile.getEffectiveUserId() != null ? userProfile.getEffectiveUserId() : "") : - Map.of(); - String payload = CookieSignatureUtil.toString(map); + TinyMap.Builder builder = TinyMap.builder(); + if (userProfile != null) { + builder.putIfNotNull("user_id", userProfile.getUserId()); + builder.putIfNotNull("e_user_id", userProfile.getEffectiveUserId()); + } + logger.log(Level.FINEST, "map for cookie payload = " + builder.build()); + String payload = CookieSignatureUtil.toString(builder.build()); String sig = CookieSignatureUtil.hmac(payload, sessionSecret, sessionCookieAlgorithm); String cookieValue = String.join(":", id, payload, sig); PercentEncoder percentEncoder = PercentEncoders.getCookieEncoder(StandardCharsets.ISO_8859_1); diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/PersistSessionHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/PersistSessionHandler.java index b7a8574..28b3a78 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/PersistSessionHandler.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/PersistSessionHandler.java @@ -3,6 +3,8 @@ package org.xbib.net.http.server.session; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; + +import org.xbib.net.UserProfile; import org.xbib.net.http.HttpResponseStatus; import org.xbib.net.http.server.HttpException; 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 final Codec userProfileCodec; + private final Codec sessionCodec; - public PersistSessionHandler(Codec sessionCodec) { + public PersistSessionHandler(Codec userProfileCodec, + Codec sessionCodec) { + this.userProfileCodec = userProfileCodec; this.sessionCodec = sessionCodec; } @Override 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"); if (session != null) { try { diff --git a/net-http-server/src/test/java/module-info.java b/net-http-server/src/test/java/module-info.java index 688ad57..e8ba566 100644 --- a/net-http-server/src/test/java/module-info.java +++ b/net-http-server/src/test/java/module-info.java @@ -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.ldap; 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.ldap; opens org.xbib.net.http.server.test.session; + opens org.xbib.net.http.server.test.userprofile; } diff --git a/net-http-server/src/test/java/org/xbib/net/http/server/test/session/JsonSessionTest.java b/net-http-server/src/test/java/org/xbib/net/http/server/test/session/JsonSessionTest.java index 9db0a63..bf480a6 100644 --- a/net-http-server/src/test/java/org/xbib/net/http/server/test/session/JsonSessionTest.java +++ b/net-http-server/src/test/java/org/xbib/net/http/server/test/session/JsonSessionTest.java @@ -4,17 +4,16 @@ import java.io.IOException; import java.nio.file.Paths; import java.time.Duration; import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.logging.Logger; + import org.junit.jupiter.api.Test; import org.xbib.net.http.server.persist.Codec; 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; -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 void testJsonSession() throws IOException { @@ -23,12 +22,13 @@ public class JsonSessionTest { session.put("a", "b"); sessionCodec.write(session.id(), session); Session session1 = sessionCodec.read(session.id()); - logger.log(Level.INFO, session1.get("a").toString()); + assertEquals("b", session1.get("a").toString()); + sessionCodec.destroy(); } private Codec newSessionCodec() { 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 sessionCodec, Supplier sessionIdGenerator) throws IOException { diff --git a/net-http-server/src/test/java/org/xbib/net/http/server/test/userprofile/JsonUserProfileTest.java b/net-http-server/src/test/java/org/xbib/net/http/server/test/userprofile/JsonUserProfileTest.java new file mode 100644 index 0000000..8c809f3 --- /dev/null +++ b/net-http-server/src/test/java/org/xbib/net/http/server/test/userprofile/JsonUserProfileTest.java @@ -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 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 newUserProfileCodec() { + return new FileJsonUserProfileCodec(Paths.get("build/userprofile-test")); + } + + private UserProfile create(Codec userProfileCodec) throws IOException { + return userProfileCodec.create("1"); + } +}