add user profile persistence, for adding user preferences
This commit is contained in:
parent
b7417c9798
commit
1ad8f686df
23 changed files with 466 additions and 98 deletions
|
@ -1,3 +1,3 @@
|
|||
group = org.xbib
|
||||
name = net-http
|
||||
version = 4.6.0
|
||||
version = 4.7.0
|
||||
|
|
|
@ -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<UserProfile> newUserProfileCodec(HttpRouterContext httpRouterContext) {
|
||||
return new FileJsonUserProfileCodec(Paths.get("/var/tmp/userprofile"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<Session> sessionCodec = newSessionCodec(httpRouterContext);
|
||||
Codec<UserProfile> userProfileCodec = newUserProfileCodec(httpRouterContext);
|
||||
httpRouterContext.getAttributes().put("userprofilecodec", userProfileCodec);
|
||||
Codec<Session> 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<Session> sessionCodec) {
|
||||
return new IncomingContextHandler(
|
||||
protected Codec<UserProfile> newUserProfileCodec(HttpRouterContext httpRouterContext) {
|
||||
return new MemoryPropertiesUserProfileCodec();
|
||||
}
|
||||
|
||||
protected HttpHandler newIncomingContextHandler(Codec<UserProfile> userProfileCodec, Codec<Session> sessionCodec) {
|
||||
return new IncomingContextHandler(userProfileCodec,
|
||||
getSecret(),
|
||||
"HmacSHA1",
|
||||
sessionName,
|
||||
|
@ -235,8 +243,8 @@ public class BaseApplication implements Application {
|
|||
);
|
||||
}
|
||||
|
||||
protected HttpHandler newPersistHandler(Codec<Session> sessionCodec) {
|
||||
return new PersistSessionHandler(sessionCodec);
|
||||
protected HttpHandler newPersistHandler(Codec<UserProfile> userProfileCodec, Codec<Session> sessionCodec) {
|
||||
return new PersistSessionHandler(userProfileCodec, sessionCodec);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,4 +13,6 @@ public interface Codec<D> {
|
|||
void remove(String key) throws IOException;
|
||||
|
||||
void purge(long expiredAfterSeconds) throws IOException;
|
||||
|
||||
void destroy();
|
||||
}
|
||||
|
|
|
@ -67,6 +67,11 @@ public class FileJsonCodec implements Codec<Map<String, Object>> {
|
|||
// 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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -76,6 +76,11 @@ public class FilePropertiesCodec implements Codec<Map<String, Object>> {
|
|||
// 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);
|
||||
|
|
|
@ -58,6 +58,11 @@ public class MemoryPropertiesCodec implements Codec<Map<String, Object>> {
|
|||
// unable to purge
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
store.clear();
|
||||
}
|
||||
|
||||
private Map<String, Object> toMap(Properties properties) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
properties.forEach((k, v) -> map.put(k.toString(), v));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -39,6 +39,8 @@ public class BaseHttpRouterContext implements HttpRouterContext {
|
|||
|
||||
private final Attributes attributes;
|
||||
|
||||
private final List<HttpHandler> securityHandlers;
|
||||
|
||||
private final List<HttpHandler> openHandlers;
|
||||
|
||||
private final List<HttpHandler> 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);
|
||||
|
|
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<UserProfile> userProfileCodec;
|
||||
|
||||
private final String sessionSecret;
|
||||
|
||||
private final String sessionCookieAlgorithm;
|
||||
|
@ -40,12 +43,14 @@ public class IncomingContextHandler implements HttpHandler {
|
|||
|
||||
Supplier<String> sessionIdGenerator;
|
||||
|
||||
public IncomingContextHandler(String sessionSecret,
|
||||
public IncomingContextHandler(Codec<UserProfile> userProfileCodec,
|
||||
String sessionSecret,
|
||||
String sessionCookieAlgorithm,
|
||||
String sessionCookieName,
|
||||
Codec<Session> sessionCodec,
|
||||
Set<String> suffixes,
|
||||
Supplier<String> sessionIdGenerator) {
|
||||
this.userProfileCodec = userProfileCodec;
|
||||
this.sessionSecret = sessionSecret;
|
||||
this.sessionCookieAlgorithm = sessionCookieAlgorithm;
|
||||
this.sessionCookieName = sessionCookieName;
|
||||
|
@ -61,8 +66,8 @@ public class IncomingContextHandler implements HttpHandler {
|
|||
return;
|
||||
}
|
||||
Map<String, Object> 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<String, Object> map) {
|
||||
if (map == null) {
|
||||
protected Session toSession(Map<String, Object> 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<String, Object> map = (Map<String, Object>) session.get("userprofile");
|
||||
return map != null ? BaseUserProfile.fromMap(map) : null;
|
||||
}
|
||||
|
||||
@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
|
||||
protected UserProfile recoverUserProfile(HttpRouterContext context,
|
||||
Session session,
|
||||
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");
|
||||
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<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")) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Session> {
|
||||
|
@ -132,6 +129,11 @@ public class JdbcSessionCodec implements Codec<Session> {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
private String readString(String key) throws SQLException {
|
||||
List<String> list = new ArrayList<>();
|
||||
Connection connection = dataSource.getConnection();
|
|
@ -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<Session> {
|
||||
|
||||
|
@ -101,4 +98,10 @@ public class MemoryPropertiesSessionCodec implements Codec<Session> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
store.clear();
|
||||
|
||||
}
|
||||
}
|
|
@ -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<String, Object> 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<String, Object> 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);
|
||||
|
|
|
@ -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<UserProfile> userProfileCodec;
|
||||
|
||||
private final Codec<Session> sessionCodec;
|
||||
|
||||
public PersistSessionHandler(Codec<Session> sessionCodec) {
|
||||
public PersistSessionHandler(Codec<UserProfile> userProfileCodec,
|
||||
Codec<Session> 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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<Session> 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<Session> sessionCodec, Supplier<String> sessionIdGenerator) throws IOException {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue