Compare commits
10 commits
9baa804bc2
...
ab9cba9d99
Author | SHA1 | Date | |
---|---|---|---|
ab9cba9d99 | |||
4d844e4347 | |||
be8f6ef4b3 | |||
f0d743642a | |||
7d8205dd7a | |||
1ad8f686df | |||
b7417c9798 | |||
6b774d3a4d | |||
2e83b1754c | |||
54ce2c8074 |
42 changed files with 865 additions and 240 deletions
|
@ -1,3 +1,3 @@
|
||||||
group = org.xbib
|
group = org.xbib
|
||||||
name = net-http
|
name = net-http
|
||||||
version = 4.5.0
|
version = 4.8.1
|
||||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
20
gradlew.bat
vendored
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,15 +24,19 @@ 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)
|
||||||
|
throws IOException {
|
||||||
|
try {
|
||||||
this.request = context.getRequest();
|
this.request = context.getRequest();
|
||||||
Objects.requireNonNull(responseBuilder);
|
Objects.requireNonNull(responseBuilder);
|
||||||
DataBuffer dataBuffer = context.getDataBufferFactory().allocateBuffer();
|
DataBuffer dataBuffer = context.getDataBufferFactory().allocateBuffer();
|
||||||
|
@ -43,6 +51,13 @@ public class J2HtmlResource extends HtmlTemplateResource {
|
||||||
.setHeader(CONTENT_TYPE, "text/html; charset=" + getCharset().displayName())
|
.setHeader(CONTENT_TYPE, "text/html; charset=" + getCharset().displayName())
|
||||||
.body(dataBuffer)
|
.body(dataBuffer)
|
||||||
.done();
|
.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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"))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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.addOpenHandler(newIncomingContextHandler(userProfileCodec, sessionCodec));
|
||||||
httpRouterContext.addCloseHandler(newOutgoingContextHandler());
|
httpRouterContext.addCloseHandler(newOutgoingContextHandler());
|
||||||
httpRouterContext.addCloseHandler(newPersistHandler(sessionCodec));
|
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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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")) {
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
if (map.containsKey(USER_ID)) {
|
||||||
|
String userId = (String) map.get(USER_ID);
|
||||||
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(EFFECTIVE_PERMISSIONS)) {
|
||||||
|
userProfile.setEffectivePermissions((Collection<String>) map.get(EFFECTIVE_PERMISSIONS));
|
||||||
|
}
|
||||||
|
if (map.containsKey(ATTRIBUTES)) {
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
if (map.containsKey("e_perms")) {
|
|
||||||
userProfile.setEffectivePermissions((Collection<String>) map.get("e_perms"));
|
|
||||||
}
|
}
|
||||||
if (map.containsKey("attrs")) {
|
|
||||||
userProfile.setAttributes(new BaseAttributes((Map<String, Object>) map.get("attrs")));
|
|
||||||
}
|
}
|
||||||
return userProfile;
|
return userProfile;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
userProfile.setEffectiveUserId(userDetails.getEffectiveUserId());
|
||||||
|
} else {
|
||||||
|
userProfile.setEffectiveUserId(authContext.getUsername());
|
||||||
|
}
|
||||||
|
loadRoles(userProfile);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
logger.log(Level.FINE, "authentication failure for user " + username);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadRoles(UserProfile userProfile) {
|
||||||
|
GroupsProvider.Context groupContext = new GroupsProvider.Context(userProfile.getUserId(), null);
|
||||||
Collection<String> groups = securityRealm.getGroupsProvider().getGroups(groupContext);
|
Collection<String> groups = securityRealm.getGroupsProvider().getGroups(groupContext);
|
||||||
for (String group : groups) {
|
for (String group : groups) {
|
||||||
userProfile.addRole(group);
|
userProfile.addRole(group);
|
||||||
}
|
}
|
||||||
logger.log(Level.FINE, "authentication success: userProfile = " + userProfile);
|
if (!userProfile.getUserId().equals(userProfile.getEffectiveUserId())) {
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.log(Level.FINE, "authentication failure");
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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,6 +60,7 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(HttpRouterContext context) throws IOException {
|
public void handle(HttpRouterContext context) throws IOException {
|
||||||
|
try {
|
||||||
logger.log(Level.FINEST, () -> "handle: before creating resource " + this.getClass().getName());
|
logger.log(Level.FINEST, () -> "handle: before creating resource " + this.getClass().getName());
|
||||||
Resource resource = createResource(context);
|
Resource resource = createResource(context);
|
||||||
logger.log(Level.FINEST, () -> "handle: resource = " + (resource != null ? resource.getClass().getName() + " " + resource : null));
|
logger.log(Level.FINEST, () -> "handle: resource = " + (resource != null ? resource.getClass().getName() + " " + resource : null));
|
||||||
|
@ -90,6 +95,13 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
||||||
generateCacheableResource(context, resource);
|
generateCacheableResource(context, resource);
|
||||||
context.done();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void generateCacheableResource(HttpRouterContext context,
|
private void generateCacheableResource(HttpRouterContext context,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
if (session != null) {
|
||||||
logger.log(Level.FINE, "new session created, id = " + session.id());
|
logger.log(Level.FINE, "new session created, id = " + session.id());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
context.getAttributes().put("session", session);
|
context.getAttributes().put("session", session);
|
||||||
if (userProfile == null) {
|
if (userProfile == null) {
|
||||||
userProfile = newUserProfile(payload);
|
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,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// initialize
|
||||||
|
Map<String, Object> map = null;
|
||||||
|
// is user profile present in session?
|
||||||
|
if (payload == null && session != null) {
|
||||||
|
// session has stored user profile? this is important for spanning HTTP request/response like in HTTP POST/FORWARD/GET
|
||||||
|
map = (Map<String, Object>) session.get("userprofile");
|
||||||
|
} else if (payload != null) {
|
||||||
|
// is a mini user profile otherwise present in our cookie?
|
||||||
|
map = (Map<String, Object>) payload.get("map");
|
||||||
|
}
|
||||||
|
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;
|
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
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue