Compare commits

...

10 commits

42 changed files with 865 additions and 240 deletions

View file

@ -1,3 +1,3 @@
group = org.xbib group = org.xbib
name = net-http name = net-http
version = 4.5.0 version = 4.8.1

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

20
gradlew.bat vendored
View file

@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View file

@ -0,0 +1,41 @@
package org.xbib.net.http.j2html;
import org.xbib.net.ParameterException;
import org.xbib.net.Resource;
import org.xbib.net.http.HttpResponseStatus;
import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler;
import org.xbib.net.http.server.route.HttpRouterContext;
import java.io.IOException;
import static org.xbib.j2html.TagCreator.body;
import static org.xbib.j2html.TagCreator.h1;
import static org.xbib.j2html.TagCreator.html;
public class BadRequestHandler extends J2HtmlResourceHandler {
@Override
protected Resource createResource(HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
return new UnauthorizedResource(this, httpRouterContext);
}
protected static class UnauthorizedResource extends J2HtmlResource {
protected UnauthorizedResource(HtmlTemplateResourceHandler templateResourceHandler,
HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
super(templateResourceHandler, httpRouterContext);
}
@Override
protected HttpResponseStatus getResponseStatus() {
return HttpResponseStatus.BAD_REQUEST;
}
@Override
protected String renderHtml(HttpRouterContext httpRouterContext) {
return html(body(h1("Bad request"))).render();
}
}
}

View file

@ -1,6 +1,7 @@
package org.xbib.net.http.j2html; package org.xbib.net.http.j2html;
import org.xbib.net.Attributes; import org.xbib.net.Attributes;
import org.xbib.net.ParameterException;
import org.xbib.net.Resource; import org.xbib.net.Resource;
import org.xbib.net.http.HttpResponseStatus; import org.xbib.net.http.HttpResponseStatus;
import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler; import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler;
@ -19,14 +20,16 @@ import static org.xbib.j2html.TagCreator.pre;
public class InternalServerErrorHandler extends J2HtmlResourceHandler { public class InternalServerErrorHandler extends J2HtmlResourceHandler {
@Override @Override
protected Resource createResource(HttpRouterContext httpRouterContext) throws IOException { protected Resource createResource(HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
return new InternalServerErrorResource(this, httpRouterContext); return new InternalServerErrorResource(this, httpRouterContext);
} }
protected static class InternalServerErrorResource extends J2HtmlResource { protected static class InternalServerErrorResource extends J2HtmlResource {
protected InternalServerErrorResource(HtmlTemplateResourceHandler templateResourceHandler, protected InternalServerErrorResource(HtmlTemplateResourceHandler templateResourceHandler,
HttpRouterContext httpRouterContext) throws IOException { HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
super(templateResourceHandler, httpRouterContext); super(templateResourceHandler, httpRouterContext);
} }

View file

@ -1,6 +1,8 @@
package org.xbib.net.http.j2html; package org.xbib.net.http.j2html;
import org.xbib.net.ParameterException;
import org.xbib.net.buffer.DataBuffer; import org.xbib.net.buffer.DataBuffer;
import org.xbib.net.http.HttpHeaderValues;
import org.xbib.net.http.HttpResponseStatus; import org.xbib.net.http.HttpResponseStatus;
import org.xbib.net.http.server.HttpRequest; import org.xbib.net.http.server.HttpRequest;
import org.xbib.net.http.server.resource.HtmlTemplateResource; import org.xbib.net.http.server.resource.HtmlTemplateResource;
@ -11,6 +13,8 @@ import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Objects; import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.xbib.j2html.TagCreator.body; import static org.xbib.j2html.TagCreator.body;
import static org.xbib.j2html.TagCreator.document; import static org.xbib.j2html.TagCreator.document;
@ -20,29 +24,40 @@ import static org.xbib.net.http.HttpHeaderNames.CONTENT_TYPE;
public class J2HtmlResource extends HtmlTemplateResource { public class J2HtmlResource extends HtmlTemplateResource {
private static final Logger logger = Logger.getLogger(J2HtmlResource.class.getName());
protected HttpRequest request; protected HttpRequest request;
public J2HtmlResource(HtmlTemplateResourceHandler templateResourceHandler, public J2HtmlResource(HtmlTemplateResourceHandler templateResourceHandler,
HttpRouterContext httpRouterContext) throws IOException { HttpRouterContext httpRouterContext) throws IOException, ParameterException {
super(templateResourceHandler, httpRouterContext); super(templateResourceHandler, httpRouterContext);
} }
@Override @Override
public void render(HttpRouterContext context) throws IOException { public void render(HttpRouterContext context)
this.request = context.getRequest(); throws IOException {
Objects.requireNonNull(responseBuilder); try {
DataBuffer dataBuffer = context.getDataBufferFactory().allocateBuffer(); this.request = context.getRequest();
dataBuffer.write(renderHtml(context), getCharset()); Objects.requireNonNull(responseBuilder);
HttpResponseStatus httpResponseStatus = responseBuilder.getResponseStatus(); DataBuffer dataBuffer = context.getDataBufferFactory().allocateBuffer();
if (httpResponseStatus == null) { dataBuffer.write(renderHtml(context), getCharset());
httpResponseStatus = getResponseStatus(); HttpResponseStatus httpResponseStatus = responseBuilder.getResponseStatus();
if (httpResponseStatus == null) {
httpResponseStatus = getResponseStatus();
}
context.status(httpResponseStatus)
.setHeader("cache-control", "no-cache")
.setHeader("content-length", Integer.toString(dataBuffer.writePosition()))
.setHeader(CONTENT_TYPE, "text/html; charset=" + getCharset().displayName())
.body(dataBuffer)
.done();
} catch (ParameterException e) {
logger.log(Level.WARNING, e.getMessage(), e);
context.status(HttpResponseStatus.BAD_REQUEST)
.setHeader(CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN)
.body(e.getMessage())
.done();
} }
context.status(httpResponseStatus)
.setHeader("cache-control", "no-cache")
.setHeader("content-length", Integer.toString(dataBuffer.writePosition()))
.setHeader(CONTENT_TYPE, "text/html; charset=" + getCharset().displayName())
.body(dataBuffer)
.done();
} }
protected HttpResponseStatus getResponseStatus() { protected HttpResponseStatus getResponseStatus() {
@ -54,14 +69,14 @@ public class J2HtmlResource extends HtmlTemplateResource {
} }
/** /**
* Rendering.
* By subclassing this handler, this method should be overridden by custom j2html code. * By subclassing this handler, this method should be overridden by custom j2html code.
* The HTML is here is just a greeting as an example. * The HTML is here is just a greeting as an example.
* *
* @param context the router context * @param context the router context
* @return the body string fo the HTTP response * @return the body string fo the HTTP response
*/ */
protected String renderHtml(HttpRouterContext context) { protected String renderHtml(HttpRouterContext context)
throws IOException, ParameterException {
return document(html(body(h1("Hello World")))); return document(html(body(h1("Hello World"))));
} }
} }

View file

@ -1,5 +1,6 @@
package org.xbib.net.http.j2html; package org.xbib.net.http.j2html;
import org.xbib.net.ParameterException;
import org.xbib.net.Resource; import org.xbib.net.Resource;
import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler; import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler;
import org.xbib.net.http.server.route.HttpRouterContext; import org.xbib.net.http.server.route.HttpRouterContext;
@ -7,9 +8,6 @@ import org.xbib.net.http.server.route.HttpRouterContext;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import static org.xbib.j2html.TagCreator.body;
import static org.xbib.j2html.TagCreator.h1;
public class J2HtmlResourceHandler extends HtmlTemplateResourceHandler { public class J2HtmlResourceHandler extends HtmlTemplateResourceHandler {
public J2HtmlResourceHandler() { public J2HtmlResourceHandler() {
@ -21,13 +19,15 @@ public class J2HtmlResourceHandler extends HtmlTemplateResourceHandler {
} }
@Override @Override
protected Resource createResource(HttpRouterContext httpRouterContext) throws IOException { protected Resource createResource(HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
return httpRouterContext.isAuthenticated() ? return httpRouterContext.isAuthenticated() ?
new J2HtmlResource(this, httpRouterContext) : new J2HtmlResource(this, httpRouterContext) :
createUnauthenticatedResource(httpRouterContext); createUnauthenticatedResource(httpRouterContext);
} }
protected Resource createUnauthenticatedResource(HttpRouterContext httpRouterContext) throws IOException { protected Resource createUnauthenticatedResource(HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
return new J2HtmlResource(this, httpRouterContext); return new J2HtmlResource(this, httpRouterContext);
} }
} }

View file

@ -1,5 +1,6 @@
package org.xbib.net.http.j2html; package org.xbib.net.http.j2html;
import org.xbib.net.ParameterException;
import org.xbib.net.Resource; import org.xbib.net.Resource;
import org.xbib.net.http.HttpResponseStatus; import org.xbib.net.http.HttpResponseStatus;
import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler; import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler;
@ -14,14 +15,16 @@ import static org.xbib.j2html.TagCreator.html;
public class NotFoundHandler extends J2HtmlResourceHandler { public class NotFoundHandler extends J2HtmlResourceHandler {
@Override @Override
protected Resource createResource(HttpRouterContext httpRouterContext) throws IOException { protected Resource createResource(HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
return new UnauthorizedResource(this, httpRouterContext); return new UnauthorizedResource(this, httpRouterContext);
} }
protected static class UnauthorizedResource extends J2HtmlResource { protected static class UnauthorizedResource extends J2HtmlResource {
protected UnauthorizedResource(HtmlTemplateResourceHandler templateResourceHandler, protected UnauthorizedResource(HtmlTemplateResourceHandler templateResourceHandler,
HttpRouterContext httpRouterContext) throws IOException { HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
super(templateResourceHandler, httpRouterContext); super(templateResourceHandler, httpRouterContext);
} }

View file

@ -1,5 +1,6 @@
package org.xbib.net.http.j2html; package org.xbib.net.http.j2html;
import org.xbib.net.ParameterException;
import org.xbib.net.Resource; import org.xbib.net.Resource;
import org.xbib.net.http.HttpResponseStatus; import org.xbib.net.http.HttpResponseStatus;
import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler; import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler;
@ -14,14 +15,16 @@ import static org.xbib.j2html.TagCreator.html;
public class UnauthorizedHandler extends J2HtmlResourceHandler { public class UnauthorizedHandler extends J2HtmlResourceHandler {
@Override @Override
protected Resource createResource(HttpRouterContext httpRouterContext) throws IOException { protected Resource createResource(HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
return new UnauthorizedResource(this, httpRouterContext); return new UnauthorizedResource(this, httpRouterContext);
} }
protected static class UnauthorizedResource extends J2HtmlResource { protected static class UnauthorizedResource extends J2HtmlResource {
protected UnauthorizedResource(HtmlTemplateResourceHandler templateResourceHandler, protected UnauthorizedResource(HtmlTemplateResourceHandler templateResourceHandler,
HttpRouterContext httpRouterContext) throws IOException { HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
super(templateResourceHandler, httpRouterContext); super(templateResourceHandler, httpRouterContext);
} }

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

@ -5,15 +5,19 @@ import org.xbib.config.ConfigLogger;
import org.xbib.config.ConfigParams; import org.xbib.config.ConfigParams;
import org.xbib.config.SystemConfigLogger; import org.xbib.config.SystemConfigLogger;
import org.xbib.net.NetworkClass; import org.xbib.net.NetworkClass;
import org.xbib.net.ParameterException;
import org.xbib.net.Resource; import org.xbib.net.Resource;
import org.xbib.net.http.HttpHeaderNames; import org.xbib.net.http.HttpHeaderNames;
import org.xbib.net.http.HttpHeaderValues; import org.xbib.net.http.HttpHeaderValues;
import org.xbib.net.http.HttpResponseStatus; import org.xbib.net.http.HttpResponseStatus;
import org.xbib.net.http.HttpVersion; import org.xbib.net.http.HttpVersion;
import org.xbib.net.http.j2html.BadRequestHandler;
import org.xbib.net.http.j2html.InternalServerErrorHandler; import org.xbib.net.http.j2html.InternalServerErrorHandler;
import org.xbib.net.http.j2html.J2HtmlResource; import org.xbib.net.http.j2html.J2HtmlResource;
import org.xbib.net.http.j2html.J2HtmlResourceHandler; import org.xbib.net.http.j2html.J2HtmlResourceHandler;
import org.xbib.net.http.j2html.J2HtmlService; import org.xbib.net.http.j2html.J2HtmlService;
import org.xbib.net.http.j2html.NotFoundHandler;
import org.xbib.net.http.j2html.UnauthorizedHandler;
import org.xbib.net.http.server.application.web.WebApplication; import org.xbib.net.http.server.application.web.WebApplication;
import org.xbib.net.http.server.auth.BasicAuthenticationHandler; import org.xbib.net.http.server.auth.BasicAuthenticationHandler;
import org.xbib.net.http.server.auth.FormAuthenticationHandler; import org.xbib.net.http.server.auth.FormAuthenticationHandler;
@ -141,6 +145,9 @@ public final class Bootstrap {
.build(); .build();
HttpRouter httpRouter = BaseHttpRouter.builder() HttpRouter httpRouter = BaseHttpRouter.builder()
.setHandler(401, new UnauthorizedHandler())
.setHandler(403, new BadRequestHandler())
.setHandler(404, new NotFoundHandler())
.setHandler(500, new InternalServerErrorHandler()) .setHandler(500, new InternalServerErrorHandler())
.addDomain(BaseHttpDomain.builder() .addDomain(BaseHttpDomain.builder()
.setHttpAddress(httpsAddress) .setHttpAddress(httpsAddress)
@ -206,12 +213,14 @@ public final class Bootstrap {
static class MyResourceHandler extends J2HtmlResourceHandler { static class MyResourceHandler extends J2HtmlResourceHandler {
@Override @Override
protected Resource createResource(HttpRouterContext httpRouterContext) throws IOException { protected Resource createResource(HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
return new DemoResource(this, httpRouterContext); return new DemoResource(this, httpRouterContext);
} }
@Override @Override
protected Resource createUnauthenticatedResource(HttpRouterContext httpRouterContext) throws IOException { protected Resource createUnauthenticatedResource(HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
return new UnauthenticatedDemoResource(this, httpRouterContext); return new UnauthenticatedDemoResource(this, httpRouterContext);
} }
} }
@ -219,7 +228,8 @@ public final class Bootstrap {
static class DemoResource extends J2HtmlResource { static class DemoResource extends J2HtmlResource {
public DemoResource(HtmlTemplateResourceHandler templateResourceHandler, public DemoResource(HtmlTemplateResourceHandler templateResourceHandler,
HttpRouterContext httpRouterContext) throws IOException { HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
super(templateResourceHandler, httpRouterContext); super(templateResourceHandler, httpRouterContext);
} }
@ -233,7 +243,8 @@ public final class Bootstrap {
static class UnauthenticatedDemoResource extends J2HtmlResource { static class UnauthenticatedDemoResource extends J2HtmlResource {
public UnauthenticatedDemoResource(HtmlTemplateResourceHandler templateResourceHandler, public UnauthenticatedDemoResource(HtmlTemplateResourceHandler templateResourceHandler,
HttpRouterContext httpRouterContext) throws IOException { HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
super(templateResourceHandler, httpRouterContext); super(templateResourceHandler, httpRouterContext);
} }

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

@ -3,6 +3,8 @@ package org.xbib.net.http.server.application;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Locale; import java.util.Locale;
import org.xbib.net.http.server.domain.HttpSecurityDomain;
import org.xbib.net.http.server.executor.Executor; import org.xbib.net.http.server.executor.Executor;
import org.xbib.net.http.server.route.HttpRouter; import org.xbib.net.http.server.route.HttpRouter;
import org.xbib.net.mime.MimeTypeService; import org.xbib.net.mime.MimeTypeService;
@ -15,6 +17,8 @@ public interface ApplicationBuilder {
ApplicationBuilder setSecret(String hexSecret); ApplicationBuilder setSecret(String hexSecret);
ApplicationBuilder setSecurityDomain(HttpSecurityDomain securityDomain);
ApplicationBuilder setSessionsEnabled(boolean sessionsEnabled); ApplicationBuilder setSessionsEnabled(boolean sessionsEnabled);
ApplicationBuilder setLocale(Locale locale); ApplicationBuilder setLocale(Locale locale);

View file

@ -15,9 +15,11 @@ 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;
import org.xbib.net.http.server.auth.PersistUserProfileHandler;
import org.xbib.net.http.server.route.BaseHttpRouterContext; import org.xbib.net.http.server.route.BaseHttpRouterContext;
import org.xbib.net.http.server.HttpHandler; import org.xbib.net.http.server.HttpHandler;
import org.xbib.net.http.server.HttpRequestBuilder; import org.xbib.net.http.server.HttpRequestBuilder;
@ -34,7 +36,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,12 +171,16 @@ 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.addOpenHandler(newIncomingContextHandler(userProfileCodec, sessionCodec));
httpRouterContext.addCloseHandler(newPersistHandler(sessionCodec)); httpRouterContext.addCloseHandler(newOutgoingContextHandler());
for (HttpHandler httpHandler : newPersistHandlers(userProfileCodec, sessionCodec)) {
httpRouterContext.addCloseHandler(httpHandler);
} }
httpRouterContext.addCloseHandler(newOutgoingCookieHandler()); httpRouterContext.addCloseHandler(newOutgoingCookieHandler());
return httpRouterContext; return httpRouterContext;
@ -212,8 +219,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 +246,10 @@ public class BaseApplication implements Application {
); );
} }
protected HttpHandler newPersistHandler(Codec<Session> sessionCodec) { protected Collection<HttpHandler> newPersistHandlers(Codec<UserProfile> userProfileCodec,
return new PersistSessionHandler(sessionCodec); Codec<Session> sessionCodec) {
return List.of(new PersistSessionHandler(sessionCodec),
new PersistUserProfileHandler(userProfileCodec, builder.securityDomain));
} }
@Override @Override

View file

@ -7,6 +7,7 @@ import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import org.xbib.net.http.server.domain.HttpSecurityDomain;
import org.xbib.net.http.server.executor.BaseExecutor; import org.xbib.net.http.server.executor.BaseExecutor;
import org.xbib.net.http.server.executor.Executor; import org.xbib.net.http.server.executor.Executor;
import org.xbib.net.http.server.route.HttpRouter; import org.xbib.net.http.server.route.HttpRouter;
@ -32,6 +33,8 @@ public class BaseApplicationBuilder implements ApplicationBuilder {
protected String secret; protected String secret;
protected HttpSecurityDomain securityDomain;
protected boolean sessionsEnabled; protected boolean sessionsEnabled;
protected Locale locale; protected Locale locale;
@ -87,6 +90,12 @@ public class BaseApplicationBuilder implements ApplicationBuilder {
return this; return this;
} }
@Override
public ApplicationBuilder setSecurityDomain(HttpSecurityDomain securityDomain) {
this.securityDomain = securityDomain;
return this;
}
@Override @Override
public ApplicationBuilder setSessionsEnabled(boolean sessionsEnabled) { public ApplicationBuilder setSessionsEnabled(boolean sessionsEnabled) {
this.sessionsEnabled = sessionsEnabled; this.sessionsEnabled = sessionsEnabled;

View file

@ -26,12 +26,15 @@ public class BaseUserProfile implements UserProfile {
private Collection<String> effectivePermissions; private Collection<String> effectivePermissions;
private boolean refresh;
public BaseUserProfile() { public BaseUserProfile() {
this.attributes = new BaseAttributes(); this.attributes = new BaseAttributes();
this.roles = new ArrayList<>(); this.roles = new ArrayList<>();
this.effectiveRoles = new ArrayList<>(); this.effectiveRoles = new ArrayList<>();
this.permissions = new ArrayList<>(); this.permissions = new ArrayList<>();
this.effectivePermissions = new ArrayList<>(); this.effectivePermissions = new ArrayList<>();
this.refresh = false;
} }
@Override @Override
@ -129,6 +132,21 @@ public class BaseUserProfile implements UserProfile {
this.attributes = attributes; this.attributes = attributes;
} }
@Override
public void updateAttribute(String key, Object value) {
attributes.put(key, value);
}
@Override
public void triggerRefresh(boolean refresh) {
this.refresh = refresh;
}
@Override
public boolean isRefreshTriggered() {
return refresh;
}
@Override @Override
public Attributes getAttributes() { public Attributes getAttributes() {
return attributes; return attributes;
@ -137,31 +155,23 @@ public class BaseUserProfile implements UserProfile {
@Override @Override
public Map<String, Object> asMap() { public Map<String, Object> asMap() {
TinyMap.Builder<String, Object> builder = TinyMap.builder(); TinyMap.Builder<String, Object> builder = TinyMap.builder();
builder.put("name", getName()); builder.putIfNotNull(NAME, getName());
String userId = getUserId(); builder.putIfNotNull(USER_ID, getUserId());
if (userId == null) { builder.putIfNotNull(EFFECTIVE_USER_ID, getEffectiveUserId());
userId = "";
}
builder.put("user_id", userId);
String eUserId = getEffectiveUserId();
if (eUserId == null) {
eUserId = "";
}
builder.put("e_user_id", eUserId);
if (getRoles() != null && !getRoles().isEmpty()) { if (getRoles() != null && !getRoles().isEmpty()) {
builder.put("roles", getRoles()); builder.put(ROLES, getRoles());
} }
if (getEffectiveRoles() != null && !getEffectiveRoles().isEmpty()) { if (getEffectiveRoles() != null && !getEffectiveRoles().isEmpty()) {
builder.put("e_roles", getEffectiveRoles()); builder.put(EFFECTIVE_ROLES, getEffectiveRoles());
} }
if (getPermissions() != null && !getPermissions().isEmpty()) { if (getPermissions() != null && !getPermissions().isEmpty()) {
builder.put("perms", getPermissions()); builder.put(PERMISSIONS, getPermissions());
} }
if (getEffectivePermissions() != null && !getEffectivePermissions().isEmpty()) { if (getEffectivePermissions() != null && !getEffectivePermissions().isEmpty()) {
builder.put("e_perms", getEffectivePermissions()); builder.put(EFFECTIVE_PERMISSIONS, getEffectivePermissions());
} }
if (getAttributes() != null && !getAttributes().isEmpty()) { if (getAttributes() != null && !getAttributes().isEmpty()) {
builder.put("attrs", getAttributes()); builder.put(ATTRIBUTES, getAttributes());
} }
return builder.build(); return builder.build();
} }
@ -169,39 +179,66 @@ public class BaseUserProfile implements UserProfile {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static UserProfile fromMap(Map<String, Object> map) { public static UserProfile fromMap(Map<String, Object> map) {
BaseUserProfile userProfile = new BaseUserProfile(); BaseUserProfile userProfile = new BaseUserProfile();
if (map.containsKey("name")) { if (map.containsKey(NAME)) {
userProfile.setName((String) map.get("name")); userProfile.setName((String) map.get(NAME));
} }
if (map.containsKey("user_id")) { if (map.containsKey(USER_ID)) {
String userId = (String) map.get("user_id"); String userId = (String) map.get(USER_ID);
// empty user ID for better map transport, change it to null
if (userId != null && userId.isEmpty()) {
userId = null;
}
userProfile.setUserId(userId); userProfile.setUserId(userId);
} }
if (map.containsKey("e_user_id")) { if (map.containsKey(EFFECTIVE_USER_ID)) {
String eUserId = (String) map.get("e_user_id"); String eUserId = (String) map.get(EFFECTIVE_USER_ID);
// empty effective user ID for better map transport, change it to null
if (eUserId != null && eUserId.isEmpty()) {
eUserId = null;
}
userProfile.setEffectiveUserId(eUserId); userProfile.setEffectiveUserId(eUserId);
} }
if (map.containsKey("roles")) { if (map.containsKey(ROLES)) {
userProfile.setRoles((Collection<String>) map.get("roles")); userProfile.setRoles((Collection<String>) map.get(ROLES));
} }
if (map.containsKey("e_roles")) { if (map.containsKey(EFFECTIVE_ROLES)) {
userProfile.setEffectiveRoles((Collection<String>) map.get("e_roles")); userProfile.setEffectiveRoles((Collection<String>) map.get(EFFECTIVE_ROLES));
} }
if (map.containsKey("perms")) { if (map.containsKey(PERMISSIONS)) {
userProfile.setPermissions((Collection<String>) map.get("perms")); userProfile.setPermissions((Collection<String>) map.get(PERMISSIONS));
} }
if (map.containsKey("e_perms")) { if (map.containsKey(EFFECTIVE_PERMISSIONS)) {
userProfile.setEffectivePermissions((Collection<String>) map.get("e_perms")); userProfile.setEffectivePermissions((Collection<String>) map.get(EFFECTIVE_PERMISSIONS));
} }
if (map.containsKey("attrs")) { if (map.containsKey(ATTRIBUTES)) {
userProfile.setAttributes(new BaseAttributes((Map<String, Object>) map.get("attrs"))); userProfile.setAttributes(new BaseAttributes((Map<String, Object>) map.get(ATTRIBUTES)));
}
return userProfile;
}
@SuppressWarnings("unchecked")
public static UserProfile fromMap(UserProfile userProfile, Map<String, Object> map) {
if (map.containsKey(NAME)) {
userProfile.setName((String) map.get(NAME));
}
if (map.containsKey(USER_ID)) {
userProfile.setUserId((String) map.get(USER_ID));
}
if (map.containsKey(EFFECTIVE_USER_ID)) {
userProfile.setEffectiveUserId((String) map.get(EFFECTIVE_USER_ID));
}
if (map.containsKey(ROLES)) {
userProfile.setRoles((Collection<String>) map.get(ROLES));
}
if (map.containsKey(EFFECTIVE_ROLES)) {
userProfile.setEffectiveRoles((Collection<String>) map.get(EFFECTIVE_ROLES));
}
if (map.containsKey(PERMISSIONS)) {
userProfile.setPermissions((Collection<String>) map.get(PERMISSIONS));
}
if (map.containsKey(EFFECTIVE_PERMISSIONS)) {
userProfile.setEffectivePermissions((Collection<String>) map.get(EFFECTIVE_PERMISSIONS));
}
if (map.containsKey(ATTRIBUTES)) {
if (userProfile.getAttributes() == null || userProfile.getAttributes().isEmpty()) {
userProfile.setAttributes(new BaseAttributes((Map<String, Object>) map.get(ATTRIBUTES)));
} else {
for (Map.Entry<String, Object> entry : ((Map<String, Object>) map.get(ATTRIBUTES)).entrySet()) {
userProfile.updateAttribute(entry.getKey(), entry.getValue());
}
}
} }
return userProfile; return userProfile;
} }

View file

@ -0,0 +1,145 @@
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();
}
@Override
public UserProfile create(String userId) throws IOException {
Objects.requireNonNull(userId, "user id must not be null");
BaseUserProfile baseUserProfile = new BaseUserProfile();
baseUserProfile.setUserId(userId);
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();
createPath();
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);
}
}
private void createPath() {
try {
if (!Files.exists(path)) {
Files.createDirectories(path);
}
} catch (IOException e) {
logger.log(Level.SEVERE, e.getMessage(), e);
throw new UncheckedIOException(e);
}
}
}

View file

@ -1,7 +1,9 @@
package org.xbib.net.http.server.auth; package org.xbib.net.http.server.auth;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -54,6 +56,20 @@ public class LoginAuthenticationHandler implements HttpHandler {
logger.log(Level.FINE, "no user profile found in context"); logger.log(Level.FINE, "no user profile found in context");
return; return;
} }
if (userProfile.isRefreshTriggered()) {
try {
List<String> roles = new ArrayList<>();
userProfile.setRoles(roles);
List<String> effectiveRoles = new ArrayList<>();
userProfile.setEffectiveRoles(effectiveRoles);
loadRoles(userProfile);
} catch (Exception e) {
logger.log(Level.SEVERE, e.getMessage(), e);
} finally {
userProfile.triggerRefresh(false);
}
return;
}
if (userProfile.getUserId() != null) { if (userProfile.getUserId() != null) {
logger.log(Level.FINE, "user id already set"); logger.log(Level.FINE, "user id already set");
return; return;
@ -77,7 +93,10 @@ public class LoginAuthenticationHandler implements HttpHandler {
} }
} }
protected boolean authenticate(UserProfile userProfile, String username, String password, Request request) { protected boolean authenticate(UserProfile userProfile,
String username,
String password,
Request request) {
if (username == null) { if (username == null) {
logger.log(Level.FINE, "no username given for check, doing nothing"); logger.log(Level.FINE, "no username given for check, doing nothing");
return false; return false;
@ -89,21 +108,38 @@ public class LoginAuthenticationHandler implements HttpHandler {
Authenticator auth = securityRealm.getAuthenticator(); Authenticator auth = securityRealm.getAuthenticator();
Authenticator.Context authContext = new Authenticator.Context(username, password, request); Authenticator.Context authContext = new Authenticator.Context(username, password, request);
if (auth.authenticate(authContext)) { if (auth.authenticate(authContext)) {
logger.log(Level.FINE, "authenticated, augmenting user profile with " + authContext.getUsername()); logger.log(Level.FINE, "authenticated as " + authContext.getUsername());
userProfile.setUserId(authContext.getUsername()); userProfile.setUserId(authContext.getUsername());
UsersProvider.Context userContext = new UsersProvider.Context(username, null); UsersProvider.Context userContext = new UsersProvider.Context(authContext.getUsername(), null);
UserDetails userDetails = securityRealm.getUsersProvider().getUserDetails(userContext); UserDetails userDetails = securityRealm.getUsersProvider().getUserDetails(userContext);
userProfile.setEffectiveUserId(userDetails.getEffectiveUserId());
userProfile.setName(userDetails.getName()); userProfile.setName(userDetails.getName());
GroupsProvider.Context groupContext = new GroupsProvider.Context(username, null); if (userDetails.getEffectiveUserId() != null) {
Collection<String> groups = securityRealm.getGroupsProvider().getGroups(groupContext); userProfile.setEffectiveUserId(userDetails.getEffectiveUserId());
for (String group : groups) { } else {
userProfile.addRole(group); userProfile.setEffectiveUserId(authContext.getUsername());
} }
logger.log(Level.FINE, "authentication success: userProfile = " + userProfile); loadRoles(userProfile);
return true; return true;
} }
logger.log(Level.FINE, "authentication failure"); logger.log(Level.FINE, "authentication failure for user " + username);
return false; return false;
} }
private void loadRoles(UserProfile userProfile) {
GroupsProvider.Context groupContext = new GroupsProvider.Context(userProfile.getUserId(), null);
Collection<String> groups = securityRealm.getGroupsProvider().getGroups(groupContext);
for (String group : groups) {
userProfile.addRole(group);
}
if (!userProfile.getUserId().equals(userProfile.getEffectiveUserId())) {
GroupsProvider.Context effectiveGroupContext = new GroupsProvider.Context(userProfile.getEffectiveUserId(), null);
for (String group : securityRealm.getGroupsProvider().getGroups(effectiveGroupContext)) {
userProfile.addEffectiveRole(group);
}
} else {
for (String group : groups) {
userProfile.addEffectiveRole(group);
}
}
}
} }

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

@ -0,0 +1,91 @@
package org.xbib.net.http.server.auth;
import org.xbib.net.GroupsProvider;
import org.xbib.net.SecurityRealm;
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;
import org.xbib.net.http.server.domain.HttpSecurityDomain;
import org.xbib.net.http.server.persist.Codec;
import org.xbib.net.http.server.route.HttpRouterContext;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
public class PersistUserProfileHandler implements HttpHandler {
private static final Logger logger = Logger.getLogger(PersistUserProfileHandler.class.getName());
private final Codec<UserProfile> userProfileCodec;
private final HttpSecurityDomain securityDomain;
public PersistUserProfileHandler(Codec<UserProfile> userProfileCodec,
HttpSecurityDomain securityDomain) {
this.userProfileCodec = userProfileCodec;
this.securityDomain = securityDomain;
}
@Override
public void handle(HttpRouterContext context) throws IOException {
UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile");
if (userProfile != null && userProfile.getUserId() != null) {
if (userProfile.isRefreshTriggered()) {
if (securityDomain == null) {
logger.log(Level.FINE, "unable to refresh, security domain is null");
return;
}
if (securityDomain.getRealm() == null) {
logger.log(Level.FINE, "unable to refresh, security realm is null");
return;
}
try {
// reset roles and load again
List<String> roles = new ArrayList<>();
userProfile.setRoles(roles);
List<String> effectiveRoles = new ArrayList<>();
userProfile.setEffectiveRoles(effectiveRoles);
loadRoles(userProfile, securityDomain.getRealm());
} catch (Exception e) {
logger.log(Level.SEVERE, e.getMessage(), e);
} finally {
userProfile.triggerRefresh(false);
}
}
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));
}
}
private void loadRoles(UserProfile userProfile, SecurityRealm securityRealm) {
String username = userProfile.getUserId();
GroupsProvider.Context groupContext = new GroupsProvider.Context(username, null);
Collection<String> groups = securityRealm.getGroupsProvider().getGroups(groupContext);
for (String group : groups) {
userProfile.addRole(group);
}
if (!userProfile.getUserId().equals(userProfile.getEffectiveUserId())) {
GroupsProvider.Context effectiveGroupContext = new GroupsProvider.Context(userProfile.getEffectiveUserId(), null);
for (String group : securityRealm.getGroupsProvider().getGroups(effectiveGroupContext)) {
userProfile.addEffectiveRole(group);
}
} else {
for (String group : groups) {
userProfile.addEffectiveRole(group);
}
}
}
}

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

@ -2,6 +2,7 @@ package org.xbib.net.http.server.domain;
import java.util.List; import java.util.List;
import org.xbib.net.SecurityDomain; import org.xbib.net.SecurityDomain;
import org.xbib.net.UserProfile;
import org.xbib.net.http.server.HttpHandler; import org.xbib.net.http.server.HttpHandler;
public interface HttpSecurityDomain extends SecurityDomain { public interface HttpSecurityDomain extends SecurityDomain {

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

@ -18,12 +18,15 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.xbib.net.ParameterException;
import org.xbib.net.Resource; import org.xbib.net.Resource;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.net.buffer.DataBuffer; import org.xbib.net.buffer.DataBuffer;
import org.xbib.net.buffer.DataBufferFactory; import org.xbib.net.buffer.DataBufferFactory;
import org.xbib.net.buffer.DataBufferUtil; import org.xbib.net.buffer.DataBufferUtil;
import org.xbib.net.http.HttpHeaderNames; import org.xbib.net.http.HttpHeaderNames;
import org.xbib.net.http.HttpHeaderValues;
import org.xbib.net.http.HttpHeaders; import org.xbib.net.http.HttpHeaders;
import org.xbib.net.http.HttpMethod; import org.xbib.net.http.HttpMethod;
import org.xbib.net.http.HttpResponseStatus; import org.xbib.net.http.HttpResponseStatus;
@ -44,7 +47,8 @@ public abstract class AbstractResourceHandler implements HttpHandler {
public AbstractResourceHandler() { public AbstractResourceHandler() {
} }
protected abstract Resource createResource(HttpRouterContext httpRouterContext) throws IOException; protected abstract Resource createResource(HttpRouterContext httpRouterContext)
throws IOException, ParameterException;
protected abstract boolean isETagEnabled(); protected abstract boolean isETagEnabled();
@ -56,39 +60,47 @@ public abstract class AbstractResourceHandler implements HttpHandler {
@Override @Override
public void handle(HttpRouterContext context) throws IOException { public void handle(HttpRouterContext context) throws IOException {
logger.log(Level.FINEST, () -> "handle: before creating resource " + this.getClass().getName()); try {
Resource resource = createResource(context); logger.log(Level.FINEST, () -> "handle: before creating resource " + this.getClass().getName());
logger.log(Level.FINEST, () -> "handle: resource = " + (resource != null ? resource.getClass().getName() + " " + resource : null)); Resource resource = createResource(context);
if (resource instanceof HtmlTemplateResource) { logger.log(Level.FINEST, () -> "handle: resource = " + (resource != null ? resource.getClass().getName() + " " + resource : null));
generateCacheableResource(context, resource); if (resource instanceof HtmlTemplateResource) {
return;
}
if (resource == null) {
throw new HttpException("resource not found", context, HttpResponseStatus.NOT_FOUND);
} else if (resource.isDirectory()) {
logger.log(Level.FINEST, "we have a directory request");
if (!resource.getResourcePath().isEmpty() && !resource.getResourcePath().endsWith("/")) {
URL url = context.getRequestBuilder().getUrl().resolve(resource.getName() + '/');
String loc = URL.builder(url)
.query(url.getQuery())
.fragment(url.getFragment())
.build()
.toString();
logger.log(Level.FINEST, "client must add a /, external redirect to = " + loc);
context.setHeader(HttpHeaderNames.LOCATION, loc)
.status(HttpResponseStatus.TEMPORARY_REDIRECT);
} else if (resource.isExistsIndexFile()) {
// internal redirect to default index file in this directory
logger.log(Level.FINEST, "internal redirect to default index file in this directory: " + resource.getIndexFileName());
generateCacheableResource(context, resource); generateCacheableResource(context, resource);
} else { return;
// send forbidden, we do not allow directory access
context.status(HttpResponseStatus.FORBIDDEN);
} }
context.done(); if (resource == null) {
} else { throw new HttpException("resource not found", context, HttpResponseStatus.NOT_FOUND);
generateCacheableResource(context, resource); } else if (resource.isDirectory()) {
context.done(); logger.log(Level.FINEST, "we have a directory request");
if (!resource.getResourcePath().isEmpty() && !resource.getResourcePath().endsWith("/")) {
URL url = context.getRequestBuilder().getUrl().resolve(resource.getName() + '/');
String loc = URL.builder(url)
.query(url.getQuery())
.fragment(url.getFragment())
.build()
.toString();
logger.log(Level.FINEST, "client must add a /, external redirect to = " + loc);
context.setHeader(HttpHeaderNames.LOCATION, loc)
.status(HttpResponseStatus.TEMPORARY_REDIRECT);
} else if (resource.isExistsIndexFile()) {
// internal redirect to default index file in this directory
logger.log(Level.FINEST, "internal redirect to default index file in this directory: " + resource.getIndexFileName());
generateCacheableResource(context, resource);
} else {
// send forbidden, we do not allow directory access
context.status(HttpResponseStatus.FORBIDDEN);
}
context.done();
} else {
generateCacheableResource(context, resource);
context.done();
}
} catch (ParameterException e) {
logger.log(Level.WARNING, e.getMessage(), e);
context.status(HttpResponseStatus.BAD_REQUEST)
.setHeader(CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN)
.body(e.getMessage())
.done();
} }
} }

View file

@ -2,6 +2,8 @@ package org.xbib.net.http.server.resource;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import org.xbib.net.ParameterException;
import org.xbib.net.Resource; import org.xbib.net.Resource;
import org.xbib.net.http.server.route.HttpRouterContext; import org.xbib.net.http.server.route.HttpRouterContext;
@ -22,7 +24,8 @@ public class HtmlTemplateResourceHandler extends AbstractResourceHandler {
} }
@Override @Override
protected Resource createResource(HttpRouterContext httpRouterContext) throws IOException { protected Resource createResource(HttpRouterContext httpRouterContext)
throws IOException, ParameterException {
return new HtmlTemplateResource(this, httpRouterContext); return new HtmlTemplateResource(this, httpRouterContext);
} }

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> {
@ -49,12 +46,6 @@ public class FileJsonSessionCodec implements Codec<Session> {
this.sessionCacheSize = sessionCacheSize; this.sessionCacheSize = sessionCacheSize;
this.sessionDuration = sessionDuration; this.sessionDuration = sessionDuration;
this.lock = new ReentrantReadWriteLock(); this.lock = new ReentrantReadWriteLock();
try {
Files.createDirectories(path);
} catch (IOException e) {
logger.log(Level.SEVERE, e.getMessage(), e);
throw new UncheckedIOException(e);
}
} }
@Override @Override
@ -83,6 +74,7 @@ public class FileJsonSessionCodec implements Codec<Session> {
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try { try {
writeLock.lock(); writeLock.lock();
createPath();
PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8);
try (Writer writer = Files.newBufferedWriter(path.resolve(percentEncoder.encode(key)))) { try (Writer writer = Files.newBufferedWriter(path.resolve(percentEncoder.encode(key)))) {
writer.write(JsonUtil.toString(session)); writer.write(JsonUtil.toString(session));
@ -127,4 +119,37 @@ 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);
}
}
private void createPath() {
try {
if (!Files.exists(path)) {
Files.createDirectories(path);
}
} catch (IOException e) {
logger.log(Level.SEVERE, e.getMessage(), e);
throw new UncheckedIOException(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 found");
// 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 found");
(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 found");
} 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,14 @@ public class IncomingContextHandler implements HttpHandler {
} }
} }
if (session == null) { if (session == null) {
session = newSession(context); session = createSession(context);
logger.log(Level.FINE, "new session created, id = " + session.id()); if (session != null) {
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); userProfile = recoverUserProfile(context, session, payload);
logger.log(Level.FINE, "new user profile created");
} }
context.getAttributes().put("userprofile", userProfile); context.getAttributes().put("userprofile", userProfile);
} }
@ -127,11 +134,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 +146,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 +155,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 +165,37 @@ 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) {
// already in context from previous handler?
UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile");
if (userProfile != null) {
try {
return userProfileCodec.read(userProfile.getUserId());
} catch (IOException e) {
// ignore I/O error, leads to empty user profile
}
} }
Map<String, Object> map = (Map<String, Object>) session.get("userprofile"); // initialize
return map != null ? BaseUserProfile.fromMap(map) : null; Map<String, Object> map = null;
} // is user profile present in session?
if (payload == null && session != null) {
@SuppressWarnings("unchecked") // session has stored user profile? this is important for spanning HTTP request/response like in HTTP POST/FORWARD/GET
protected UserProfile newUserProfile(Map<String, Object> cookieMap) { map = (Map<String, Object>) session.get("userprofile");
UserProfile userProfile = new BaseUserProfile(); } else if (payload != null) {
// user_id, e_user_id are in cookie map // is a mini user profile otherwise present in our cookie?
if (cookieMap != null) { map = (Map<String, Object>) payload.get("map");
Map<String, Object> m = (Map<String, Object>) cookieMap.get("map");
if (m == null) {
// nothing found
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 (map != null && map.containsKey(UserProfile.USER_ID)) {
try {
return userProfileCodec.read(BaseUserProfile.fromMap(map).getUserId());
} catch (IOException e) {
// ignore I/O error, leads to incomplete user profile
}
} else {
logger.log(Level.FINE, "unable to find info for initialize user profile");
}
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,17 +128,19 @@ 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(UserProfile.USER_ID, userProfile.getUserId());
Map.of(); builder.putIfNotNull(UserProfile.EFFECTIVE_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);
DefaultCookie cookie = new DefaultCookie(sessionCookieName, percentEncoder.encode(cookieValue)); DefaultCookie cookie = new DefaultCookie(sessionCookieName, percentEncoder.encode(cookieValue));
String domain = extractDomain(host); String domain = extractDomain(host);
if ("localhost".equals(domain)) { if (domain == null || "localhost".equals(domain)) {
logger.log(Level.WARNING, "localhost not set as a cookie domain"); logger.log(Level.WARNING, "localhost not set as a cookie domain");
} else { } else {
cookie.setDomain('.' + domain); cookie.setDomain('.' + domain);
@ -153,7 +156,7 @@ public class OutgoingContextHandler implements HttpHandler {
private Cookie createEmptyCookie(String host, String path) { private Cookie createEmptyCookie(String host, String path) {
DefaultCookie cookie = new DefaultCookie(sessionCookieName); DefaultCookie cookie = new DefaultCookie(sessionCookieName);
String domain = extractDomain(host); String domain = extractDomain(host);
if ("localhost".equals(domain)) { if (domain == null || "localhost".equals(domain)) {
logger.log(Level.WARNING, "localhost not set as a cookie domain"); logger.log(Level.WARNING, "localhost not set as a cookie domain");
} else { } else {
cookie.setDomain('.' + domain); cookie.setDomain('.' + domain);
@ -168,6 +171,9 @@ public class OutgoingContextHandler implements HttpHandler {
} }
private static String extractDomain(String fqdn) { private static String extractDomain(String fqdn) {
if (fqdn == null) {
return null;
}
if ("localhost".equals(fqdn)) { if ("localhost".equals(fqdn)) {
return fqdn; return fqdn;
} }

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;
@ -22,7 +24,7 @@ public class PersistSessionHandler implements HttpHandler {
@Override @Override
public void handle(HttpRouterContext context) throws IOException { public void handle(HttpRouterContext context) throws IOException {
Session session = context.getAttributes().get(Session.class, "session"); Session session = context.getAttributes().get(Session.class, "session");
if (session != null) { if (session != null && session.hasPayload()) {
try { try {
logger.log(Level.FINEST, "writing session id " + session.id() + " keys = " + session.keySet()); logger.log(Level.FINEST, "writing session id " + session.id() + " keys = " + session.keySet());
sessionCodec.write(session.id(), session); sessionCodec.write(session.id(), session);

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");
}
}

View file

@ -1,12 +1,12 @@
dependencyResolutionManagement { dependencyResolutionManagement {
versionCatalogs { versionCatalogs {
libs { libs {
version('gradle', '8.5') version('gradle', '8.7')
version('groovy', '4.0.20') version('groovy', '4.0.21')
version('netty', '4.1.109.Final') version('netty', '4.1.111.Final')
version('netty-tcnative', '2.0.65.Final') version('netty-tcnative', '2.0.65.Final')
version('datastructures', '5.0.7') version('datastructures', '5.1.1')
version('net', '4.4.0') version('net', '4.8.0')
library('netty-codec-http2', 'io.netty', 'netty-codec-http2').versionRef('netty') library('netty-codec-http2', 'io.netty', 'netty-codec-http2').versionRef('netty')
library('netty-handler', 'io.netty', 'netty-handler').versionRef('netty') library('netty-handler', 'io.netty', 'netty-handler').versionRef('netty')
library('netty-handler-proxy', 'io.netty', 'netty-handler-proxy').versionRef('netty') library('netty-handler-proxy', 'io.netty', 'netty-handler-proxy').versionRef('netty')
@ -14,8 +14,7 @@ dependencyResolutionManagement {
library('netty-kqueue', 'io.netty', 'netty-transport-native-kqueue').versionRef('netty') library('netty-kqueue', 'io.netty', 'netty-transport-native-kqueue').versionRef('netty')
library('netty-boringssl', 'io.netty', 'netty-tcnative-boringssl-static').versionRef('netty-tcnative') library('netty-boringssl', 'io.netty', 'netty-tcnative-boringssl-static').versionRef('netty-tcnative')
library('conscrypt', 'org.conscrypt', 'conscrypt-openjdk-uber').version('2.5.2') library('conscrypt', 'org.conscrypt', 'conscrypt-openjdk-uber').version('2.5.2')
library('jackson', 'com.fasterxml.jackson.core', 'jackson-databind').version('2.16.0') library('jackson', 'com.fasterxml.jackson.core', 'jackson-databind').version('2.17.1')
library('jna', 'net.java.dev.jna', 'jna').version('5.14.0')
library('groovy-templates', 'org.apache.groovy', 'groovy-templates').versionRef('groovy') library('groovy-templates', 'org.apache.groovy', 'groovy-templates').versionRef('groovy')
library('j2html', 'org.xbib', 'j2html').version('2.0.0') library('j2html', 'org.xbib', 'j2html').version('2.0.0')
library('webjars-bootstrap', 'org.webjars', 'bootstrap').version('5.1.0') library('webjars-bootstrap', 'org.webjars', 'bootstrap').version('5.1.0')