From 1ea725cbc238e94752a926097a4b56a2d30f4621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Prante?= Date: Tue, 21 Mar 2023 16:41:49 +0100 Subject: [PATCH] working on form requests and Groovy templates, update to xbib net 3.0.4 --- .../application/web/WebApplication.java | 13 +- .../net/http/server/netty/HttpRequest.java | 6 +- .../http/server/netty/HttpRequestBuilder.java | 7 +- .../xbib/net/http/server/BaseApplication.java | 13 +- .../http/server/BaseHttpServerContext.java | 78 +++++++---- .../server/resource/HtmlTemplateResource.java | 27 ++-- .../net/http/server/route/BaseHttpRoute.java | 3 +- .../server/route/BaseHttpRouteResolver.java | 2 +- .../net/http/server/route/BaseHttpRouter.java | 16 ++- .../session/IncomingSessionHandler.java | 13 +- .../session/OutgoingSessionHandler.java | 38 ++++-- .../groovy/DefaultMarkupTemplate.java | 19 +-- ...oovyHttpResonseStatusTemplateResource.java | 27 ---- .../groovy/GroovyTemplateResource.java | 125 ++++++++++-------- settings.gradle | 2 +- 15 files changed, 231 insertions(+), 158 deletions(-) diff --git a/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java b/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java index d946a41..33a7434 100644 --- a/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java +++ b/net-http-server-application-web/src/main/java/org/xbib/net/http/server/application/web/WebApplication.java @@ -2,6 +2,8 @@ package org.xbib.net.http.server.application.web; import java.nio.file.Paths; import java.time.Duration; + +import org.xbib.net.http.cookie.SameSite; import org.xbib.net.http.server.BaseApplication; import org.xbib.net.http.server.HttpHandler; import org.xbib.net.http.server.HttpServerContext; @@ -10,6 +12,7 @@ import org.xbib.net.http.server.session.IncomingSessionHandler; import org.xbib.net.http.server.session.OutgoingSessionHandler; import org.xbib.net.http.server.session.Session; import org.xbib.net.http.server.session.file.FileJsonSessionCodec; +import org.xbib.net.util.RandomUtil; public class WebApplication extends BaseApplication { @@ -40,7 +43,8 @@ public class WebApplication extends BaseApplication { sessionCodec, getStaticFileSuffixes(), "user_id", - "e_user_id"); + "e_user_id", + () -> RandomUtil.randomString(16)); } protected OutgoingSessionHandler buildOutgoingSessionHandler(HttpServerContext httpServerContext) { @@ -50,10 +54,13 @@ public class WebApplication extends BaseApplication { getSecret(), "HmacSHA1", sessionName, - Duration.ofDays(1), sessionCodec, getStaticFileSuffixes(), "user_id", - "e_user_id"); + "e_user_id", + Duration.ofDays(1), + true, + false, + SameSite.LAX); } } diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequest.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequest.java index 3eb0494..fbde81b 100644 --- a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequest.java +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequest.java @@ -9,6 +9,7 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Objects; public class HttpRequest extends BaseHttpRequest { @@ -47,7 +48,10 @@ public class HttpRequest extends BaseHttpRequest { @Override public String toString() { - return "HttpRequest[request=" + builder.fullHttpRequest + "]"; + return "HttpRequest[request=" + builder.fullHttpRequest + + ",parameter=" + builder.getParameter() + + ",body=" + builder.fullHttpRequest.content().toString(StandardCharsets.UTF_8) + + "]"; } public ByteBuf getByteBuf() { diff --git a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequestBuilder.java b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequestBuilder.java index 9fdab43..5e49e3e 100644 --- a/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequestBuilder.java +++ b/net-http-server-netty/src/main/java/org/xbib/net/http/server/netty/HttpRequestBuilder.java @@ -2,6 +2,7 @@ package org.xbib.net.http.server.netty; import io.netty.buffer.ByteBufUtil; import io.netty.handler.codec.http.FullHttpRequest; +import org.xbib.net.Parameter; import org.xbib.net.URL; import org.xbib.net.http.HttpAddress; import org.xbib.net.http.HttpMethod; @@ -24,7 +25,7 @@ public class HttpRequestBuilder extends BaseHttpRequestBuilder { public HttpRequestBuilder setFullHttpRequest(FullHttpRequest fullHttpRequest) { if (fullHttpRequest != null) { - // retain request so we can read the body later without refCnt=0 error + // retain request, so we can read the body later without refCnt=0 error this.fullHttpRequest = fullHttpRequest.retain(); setVersion(HttpVersion.valueOf(fullHttpRequest.protocolVersion().text())); setMethod(HttpMethod.valueOf(fullHttpRequest.method().name())); @@ -100,6 +101,10 @@ public class HttpRequestBuilder extends BaseHttpRequestBuilder { return this; } + protected Parameter getParameter() { + return super.parameter; + } + @Override public HttpRequest build() { return new HttpRequest(this); diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplication.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplication.java index 02ebdce..52c6f44 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplication.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseApplication.java @@ -1,5 +1,6 @@ package org.xbib.net.http.server; +import org.xbib.net.http.cookie.SameSite; import org.xbib.net.http.server.route.HttpRouter; import org.xbib.net.http.HttpAddress; import org.xbib.net.http.HttpResponseStatus; @@ -14,6 +15,7 @@ import org.xbib.net.http.server.session.Session; import org.xbib.net.http.server.util.BlockingThreadPoolExecutor; import org.xbib.net.http.server.validate.HttpRequestValidator; import org.xbib.net.util.NamedThreadFactory; +import org.xbib.net.util.RandomUtil; import org.xbib.settings.Settings; import java.io.Closeable; @@ -194,7 +196,8 @@ public class BaseApplication implements Application { sessionCodec, getStaticFileSuffixes(), "user_id", - "e_user_id"); + "e_user_id", + () -> RandomUtil.randomString(16)); } protected HttpHandler buildOutgoingSessionHandler(HttpServerContext httpServerContext) { @@ -204,11 +207,15 @@ public class BaseApplication implements Application { getSecret(), "HmacSHA1", sessionName, - Duration.ofDays(1), sessionCodec, getStaticFileSuffixes(), "user_id", - "e_user_id"); + "e_user_id", + Duration.ofDays(1), + true, + false, + SameSite.LAX + ); } protected HttpResponseRenderer buildResponseRenderer() { diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServerContext.java b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServerContext.java index 17c4799..d72df0c 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServerContext.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/BaseHttpServerContext.java @@ -1,6 +1,5 @@ package org.xbib.net.http.server; -import java.util.Map; import org.xbib.net.Attributes; import org.xbib.net.Parameter; import org.xbib.net.ParameterBuilder; @@ -23,11 +22,15 @@ import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; import java.nio.file.Path; import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; import static org.xbib.net.http.HttpHeaderNames.CONTENT_TYPE; public class BaseHttpServerContext implements HttpServerContext { + private static final Logger logger = Logger.getLogger(BaseHttpServerContext.class.getName()); + private static final String PATH_SEPARATOR = "/"; private final Application application; @@ -84,16 +87,26 @@ public class BaseHttpServerContext implements HttpServerContext { @Override public void setResolverResult(HttpRouteResolver.Result pathResolverResult) { this.pathResolverResult = pathResolverResult; - this.attributes.put("context", pathResolverResult.getContext()); - this.attributes.put("handler", pathResolverResult.getValue()); - this.attributes.put("pathparams", pathResolverResult.getParameter()); - String contextPath = pathResolverResult.getContext() != null ? - PATH_SEPARATOR + String.join(PATH_SEPARATOR, pathResolverResult.getContext()) : null; - setContextPath(contextPath); - setContextURL(request().getBaseURL().resolve(contextPath != null ? contextPath + "/" : "")); - this.httpRequest = createRequest(httpRequestBuilder); - this.attributes.put("request", httpRequest); - this.next = false; + if (pathResolverResult != null) { + attributes.put("context", pathResolverResult.getContext()); + attributes.put("handler", pathResolverResult.getValue()); + attributes.put("pathparams", pathResolverResult.getParameter()); + String contextPath = pathResolverResult.getContext() != null ? + PATH_SEPARATOR + String.join(PATH_SEPARATOR, pathResolverResult.getContext()) : null; + setContextPath(contextPath); + setContextURL(request().getBaseURL().resolve(contextPath != null ? contextPath + "/" : "")); + } else { + // path resolver result null means "404 not found". Set default values. + attributes.put("context", null); + attributes.put("handler", null); + attributes.put("pathparams", null); + setContextPath(PATH_SEPARATOR); + setContextURL(request().getBaseURL()); + } + httpRequest = createRequest(); + logger.log(Level.FINER, "request = " + httpRequest); + attributes.put("request", httpRequest); + next = false; } public void setContextPath(String contextPath) { @@ -114,6 +127,7 @@ public class BaseHttpServerContext implements HttpServerContext { return contextURL; } + @Override public Path resolve(String path) { return application.resolve(path); } @@ -187,8 +201,8 @@ public class BaseHttpServerContext implements HttpServerContext { httpResponseBuilder.write(fileChannel, bufferSize); } - protected HttpRequest createRequest(HttpRequestBuilder requestBuilder) { - HttpHeaders headers = requestBuilder.getHeaders(); + protected HttpRequest createRequest() { + HttpHeaders headers = httpRequestBuilder.getHeaders(); String mimeType = headers.get(CONTENT_TYPE); Charset charset = StandardCharsets.UTF_8; if (mimeType != null) { @@ -198,30 +212,40 @@ public class BaseHttpServerContext implements HttpServerContext { // helper URL to collect parameters in request URI URL url = URL.builder() .charset(charset, CodingErrorAction.REPLACE) - .path(requestBuilder.getRequestURI()) + .path(httpRequestBuilder.getRequestURI()) .build(); - ParameterBuilder formParameters = Parameter.builder().domain("FORM"); + ParameterBuilder formParameterBuilder = Parameter.builder().domain("FORM"); // https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4 - if (HttpMethod.POST.equals(requestBuilder.getMethod()) && + if (HttpMethod.POST.equals(httpRequestBuilder.getMethod()) && (mimeType != null && mimeType.contains(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED))) { Charset htmlCharset = getCharset(mimeType, StandardCharsets.ISO_8859_1); - CharBuffer charBuffer = requestBuilder.getBodyAsChars(htmlCharset); + CharBuffer charBuffer = httpRequestBuilder.getBodyAsChars(htmlCharset); if (charBuffer != null) { - formParameters.addPercentEncodedBody(charBuffer.toString()); + formParameterBuilder.addPercentEncodedBody(charBuffer.toString()); } } CookieBox cookieBox = attributes.get(CookieBox.class, "incomingcookies"); - ParameterBuilder cookieParameters = Parameter.builder().domain("COOKIE"); + ParameterBuilder cookieParameterBuilder = Parameter.builder().domain("COOKIE"); if (cookieBox != null) { - cookieBox.forEach(c -> cookieParameters.add(c.name(), c.value())); + cookieBox.forEach(c -> cookieParameterBuilder.add(c.name(), c.value())); } - parameterBuilder.add(url.getQueryParams()); - parameterBuilder.add(formParameters.build()); - parameterBuilder.add(cookieParameters.build()); - parameterBuilder.add(pathResolverResult.getParameter()); - requestBuilder.setParameter(parameterBuilder.build()); - requestBuilder.setContext(this); - return requestBuilder.build(); + Parameter queryParameter = url.getQueryParams(); + logger.log(Level.FINER, "adding query parameters = " + queryParameter.getDomain() + " " + queryParameter.allToString()); + parameterBuilder.add(queryParameter); + Parameter formParameter = formParameterBuilder.build(); + logger.log(Level.FINER, "adding form parameters = " + formParameter.getDomain() + " " + formParameter.allToString()); + parameterBuilder.add(formParameter); + Parameter cookieParameter = cookieParameterBuilder.build(); + logger.log(Level.FINER, "adding cookie parameters = " + cookieParameter.getDomain() + " " + cookieParameter.allToString()); + parameterBuilder.add(cookieParameter); + if (pathResolverResult != null) { + Parameter pathParameter = pathResolverResult.getParameter(); + logger.log(Level.FINER, "adding path parameters = " + pathParameter.getDomain() + " " + pathParameter.allToString()); + parameterBuilder.add(pathParameter); + } + httpRequestBuilder.setParameter(parameterBuilder.build()); + httpRequestBuilder.setContext(this); + return httpRequestBuilder.build(); } private static Charset getCharset(String contentTypeValue, Charset defaultCharset) { diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResource.java b/net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResource.java index 35b42ab..d73160e 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResource.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/resource/HtmlTemplateResource.java @@ -39,34 +39,37 @@ public class HtmlTemplateResource implements HttpServerResource { private final String suffix; + protected final boolean negotiateLocale; + protected HtmlTemplateResource(HtmlTemplateResourceHandler templateResourceHandler, HttpServerContext httpServerContext) throws IOException { this.templateResourceHandler = templateResourceHandler; String indexFileName = templateResourceHandler.getIndexFileName(); Application application = httpServerContext.attributes().get(Application.class, "application"); + this.negotiateLocale = application.getSettings().getAsBoolean("negotiateLocale", false); Path root = templateResourceHandler.getPrefix(); root = root != null ? root : application.getHome(); if (root == null) { throw new IllegalArgumentException("no home path set for template resource resolving"); } - logger.log(Level.FINE, "class = " + getClass().getName()); - logger.log(Level.FINE, "root = " + root); this.resourcePath = httpServerContext.request().getRequestPath().substring(1); - logger.log(Level.FINE, "resource path = " + resourcePath); this.path = resourcePath.length() > 0 ? root.resolve(resourcePath) : root; - logger.log(Level.FINE, "path = " + path); - logger.log(Level.FINE, "index file name = " + indexFileName); + logger.log(Level.FINER, "class = " + getClass().getName() + + " root = " + root + + " resource path = " + resourcePath + + " path = " + path + + " index file name = " + indexFileName); this.name = path.getFileName().toString(); this.baseName = AbstractResourceHandler.basename(name); this.suffix = AbstractResourceHandler.suffix(name); if (Files.isDirectory(path)) { if (getIndexFileName() != null) { Path indexPath = path.resolve(indexFileName); - logger.log(Level.FINE, "index path = " + indexPath); + logger.log(Level.FINE, "resolved to index path = " + indexPath); if (Files.exists(indexPath)) { + logger.log(Level.FINE, "index path exists"); this.isExistsIndexFile = true; this.path = indexPath; - logger.log(Level.FINE, "index file path found = " + path); this.isDirectory = false; } else { this.isExistsIndexFile = false; @@ -81,15 +84,9 @@ public class HtmlTemplateResource implements HttpServerResource { this.isDirectory = false; } this.isExists = Files.exists(path); - logger.log(Level.FINE, "exists = " + isExists); - logger.log(Level.FINE, "isDirectory = " + isDirectory); this.url = URL.create(path.toUri().toString()); - logger.log(Level.FINE, "url = " + url); - if (isExists) { - this.lastModified = Files.getLastModifiedTime(path).toInstant(); - } else { - this.lastModified = Instant.now(); - } + logger.log(Level.FINE, "isExists = " + isExists + " isDirectory = " + isDirectory + " url = " + url); + this.lastModified = isExists ? Files.getLastModifiedTime(path).toInstant() : Instant.now(); // length will be computed at rendering time this.length = -1; } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRoute.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRoute.java index db8b57c..440fc2e 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRoute.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRoute.java @@ -63,10 +63,9 @@ public class BaseHttpRoute implements HttpRoute { @Override public boolean matches(ParameterBuilder parameterBuilder, HttpRoute requestedRoute) { - if (!(requestedRoute instanceof BaseHttpRoute)) { + if (!(requestedRoute instanceof BaseHttpRoute baseHttpRoute)) { return false; } - BaseHttpRoute baseHttpRoute = (BaseHttpRoute) requestedRoute; if (!httpAddress.equals(baseHttpRoute.getHttpAddress())) { return false; } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouteResolver.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouteResolver.java index 3818684..b17e443 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouteResolver.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouteResolver.java @@ -29,7 +29,7 @@ public class BaseHttpRouteResolver implements HttpRouteResolver { @Override public void resolve(HttpRoute httpRoute, ResultListener listener) { for (Map.Entry entry : builder.routes) { - ParameterBuilder parameterBuilder = Parameter.builder(); + ParameterBuilder parameterBuilder = Parameter.builder().domain("PATH"); boolean match = entry.getKey().matches(parameterBuilder, httpRoute); if (match && listener != null) { List list = Arrays.stream(httpRoute.getPath().replaceFirst(builder.prefix, "").split("/")) diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java index 43aacc9..bc92997 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/route/BaseHttpRouter.java @@ -20,10 +20,12 @@ import org.xbib.net.http.server.Application; import org.xbib.net.http.server.HttpDomain; import org.xbib.net.http.server.HttpException; import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.HttpRequest; import org.xbib.net.http.server.HttpRequestBuilder; import org.xbib.net.http.server.HttpResponseBuilder; import org.xbib.net.http.server.HttpServerContext; import org.xbib.net.http.server.HttpService; +import org.xbib.net.http.server.handler.InternalServerErrorHandler; import static org.xbib.net.http.HttpResponseStatus.NOT_FOUND; @@ -105,6 +107,7 @@ public class BaseHttpRouter implements HttpRouter { } if (httpRouteResolverResults.isEmpty()) { logger.log(Level.FINE, "route resolver results is empty, generating a not found message"); + httpServerContext.setResolverResult(null); routeStatus(NOT_FOUND, httpServerContext); return; } @@ -113,7 +116,8 @@ public class BaseHttpRouter implements HttpRouter { // first: create the final request httpServerContext.setResolverResult(httpRouteResolverResult); HttpService httpService = httpRouteResolverResult.getValue(); - application.getModules().forEach(module -> module.onOpen(application, httpServerContext, httpService, httpServerContext.httpRequest())); + HttpRequest httpRequest = httpServerContext.httpRequest(); + application.getModules().forEach(module -> module.onOpen(application, httpServerContext, httpService, httpRequest)); // second: security check, authentication etc. if (httpService.getSecurityDomain() != null) { logger.log(Level.FINEST, () -> "handling security domain service " + httpService); @@ -125,11 +129,12 @@ public class BaseHttpRouter implements HttpRouter { if (httpServerContext.isDone() || httpServerContext.isFailed()) { break; } - // accept service and execute service + // after security checks, accept service, open and execute service httpServerContext.attributes().put("service", httpService); application.getModules().forEach(module -> module.onOpen(application, httpServerContext, httpService)); logger.log(Level.FINEST, () -> "handling service " + httpService); httpService.handle(httpServerContext); + // if service signals that work is done, break if (httpServerContext.isDone() || httpServerContext.isFailed()) { break; } @@ -155,10 +160,17 @@ public class BaseHttpRouter implements HttpRouter { @Override public void routeStatus(HttpResponseStatus httpResponseStatus, HttpServerContext httpServerContext) { + logger.log(Level.FINER, "routing status " + httpResponseStatus); try { HttpHandler httpHandler = getHandler(httpResponseStatus); + if (httpHandler == null) { + logger.log(Level.FINER, "handler for " + httpResponseStatus + " not present, using default error handler"); + httpHandler = new InternalServerErrorHandler(); + } httpServerContext.response().reset(); httpHandler.handle(httpServerContext); + httpServerContext.done(); + logger.log(Level.FINER, "routing status " + httpResponseStatus + " done"); } catch (IOException ioe) { throw new IllegalStateException("unable to route response status, reason: " + ioe.getMessage(), ioe); } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingSessionHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingSessionHandler.java index f093bca..690acea 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingSessionHandler.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingSessionHandler.java @@ -21,9 +21,9 @@ import java.security.NoSuchAlgorithmException; import java.text.MessageFormat; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; -import org.xbib.net.util.RandomUtil; public class IncomingSessionHandler implements HttpHandler { @@ -46,13 +46,16 @@ public class IncomingSessionHandler implements HttpHandler { */ private final Set suffixes; + Supplier sessionIdGenerator; + public IncomingSessionHandler(String sessionSecret, String sessionCookieAlgorithm, String sessionCookieName, Codec sessionCodec, Set suffixes, String sessionUserName, - String sessionEffectiveUserName) { + String sessionEffectiveUserName, + Supplier sessionIdGenerator) { this.sessionSecret = sessionSecret; this.sessionCookieAlgorithm = sessionCookieAlgorithm; this.sessionCookieName = sessionCookieName; @@ -60,6 +63,7 @@ public class IncomingSessionHandler implements HttpHandler { this.suffixes = suffixes; this.sessionUserName = sessionUserName; this.sessionEffectiveUserName = sessionEffectiveUserName; + this.sessionIdGenerator = sessionIdGenerator; } @Override @@ -76,6 +80,7 @@ public class IncomingSessionHandler implements HttpHandler { if (session == null) { try { Map payload = decodeCookie(cookie); + logger.log(Level.FINER, "cookie decoded"); session = toSession(payload); UserProfile userProfile = newUserProfile(payload, session); if (userProfile != null) { @@ -96,14 +101,14 @@ public class IncomingSessionHandler implements HttpHandler { } if (session == null) { try { - session = sessionCodec.create(RandomUtil.randomString(32)); + session = sessionCodec.create(sessionIdGenerator.get()); } catch (IOException e) { logger.log(Level.SEVERE, e.getMessage(), e); throw new HttpException("unable to create session", context, HttpResponseStatus.INTERNAL_SERVER_ERROR); } } context.attributes().put("session", session); - + logger.log(Level.FINER, "incoming session " + session.id()); } private Map decodeCookie(Cookie cookie) throws IOException, diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingSessionHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingSessionHandler.java index 67cf94f..832accd 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingSessionHandler.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingSessionHandler.java @@ -47,22 +47,34 @@ public class OutgoingSessionHandler implements HttpHandler { private final String sessionEffectiveUserName; + private final boolean httpOnly; + + private final boolean secure; + + private final SameSite sameSite; + public OutgoingSessionHandler(String sessionSecret, String sessionCookieAlgorithm, String sessionCookieName, - Duration sessionDuration, Codec sessionCodec, Set suffixes, String sessionUserName, - String sessionEffectiveUserName) { + String sessionEffectiveUserName, + Duration sessionDuration, + boolean httpOnly, + boolean secure, + SameSite sameSite) { this.sessionSecret = sessionSecret; this.sessionCookieAlgorithm = sessionCookieAlgorithm; this.sessionCookieName = sessionCookieName; - this.sessionDuration = sessionDuration; this.sessionCodec = sessionCodec; this.suffixes = suffixes; this.sessionUserName = sessionUserName; this.sessionEffectiveUserName = sessionEffectiveUserName; + this.sessionDuration = sessionDuration; + this.httpOnly = httpOnly; + this.secure = secure; + this.sameSite = sameSite; } @Override @@ -137,28 +149,32 @@ public class OutgoingSessionHandler implements HttpHandler { PercentEncoder percentEncoder = PercentEncoders.getCookieEncoder(StandardCharsets.ISO_8859_1); DefaultCookie cookie = new DefaultCookie(sessionCookieName, percentEncoder.encode(cookieValue)); String domain = extractDomain(host); - if (!"localhost".equals(domain)) { + if ("localhost".equals(domain)) { + logger.log(Level.WARNING, "localhost not set as a cookie domain"); + } else { cookie.setDomain('.' + domain); } cookie.setPath(path); cookie.setMaxAge(sessionDuration.toSeconds()); - cookie.setHttpOnly(true); - cookie.setSecure(true); - cookie.setSameSite(SameSite.LAX); + cookie.setHttpOnly(httpOnly); + cookie.setSecure(secure); + cookie.setSameSite(sameSite); return cookie; } private Cookie createEmptyCookie(String host, String path) { DefaultCookie cookie = new DefaultCookie(sessionCookieName); String domain = extractDomain(host); - if (!"localhost".equals(domain)) { + if ("localhost".equals(domain)) { + logger.log(Level.WARNING, "localhost not set as a cookie domain"); + } else { cookie.setDomain('.' + domain); } cookie.setPath(path); cookie.setMaxAge(0L); - cookie.setHttpOnly(true); - cookie.setSecure(true); - cookie.setSameSite(SameSite.LAX); + cookie.setHttpOnly(httpOnly); + cookie.setSecure(secure); + cookie.setSameSite(sameSite); logger.log(Level.FINEST, "baked empty cookie"); return cookie; } diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultMarkupTemplate.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultMarkupTemplate.java index 5b31231..df2fbd6 100644 --- a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultMarkupTemplate.java +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/DefaultMarkupTemplate.java @@ -44,28 +44,31 @@ public abstract class DefaultMarkupTemplate extends BaseTemplate { super(templateEngine, model, modelTypes, configuration); this.application = (Application) model.get("application"); Objects.requireNonNull(this.application, "application must not be null"); - this.session = (Session) model.get("session"); - Objects.requireNonNull(this.session, "session must not be null"); - this.request = (HttpRequest) model.get("request"); - Objects.requireNonNull(this.request, "request must not be null"); this.responseBuilder = (HttpResponseBuilder) model.get("responsebuilder"); Objects.requireNonNull(this.responseBuilder, "response must not be null"); + this.request = (HttpRequest) model.get("request"); + // request can be null in error templates + this.session = (Session) model.get("session"); + // session can be null in error templates } - public void responseStatus(HttpResponseStatus responseStatus) { + public void setResponseStatus(HttpResponseStatus responseStatus) { responseBuilder.setResponseStatus(responseStatus); } - public void contentType(String contentType) { + public void setContentType(String contentType) { responseBuilder.setHeader(HttpHeaderNames.CONTENT_TYPE, contentType); } public boolean isContentType(String contentType) { - return request.getHeaders().containsHeader(HttpHeaderNames.CONTENT_TYPE) && contentType != null && + return request != null && + request.getHeaders() != null && + request.getHeaders().containsHeader(HttpHeaderNames.CONTENT_TYPE) && + contentType != null && request.getHeaders().get(HttpHeaderNames.CONTENT_TYPE).startsWith(contentType); } - public void contentDisposition(String contentDisposition) { + public void setContentDisposition(String contentDisposition) { responseBuilder.setHeader(HttpHeaderNames.CONTENT_DISPOSITION, contentDisposition); } diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpResonseStatusTemplateResource.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpResonseStatusTemplateResource.java index 2f5b490..06f9e63 100644 --- a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpResonseStatusTemplateResource.java +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyHttpResonseStatusTemplateResource.java @@ -2,13 +2,11 @@ package org.xbib.net.http.template.groovy; import java.util.logging.Level; import java.util.logging.Logger; -import org.xbib.net.URL; import org.xbib.net.http.HttpResponseStatus; import org.xbib.net.http.server.Application; import org.xbib.net.http.server.HttpServerContext; import java.io.IOException; -import java.nio.file.Path; class GroovyHttpResonseStatusTemplateResource extends GroovyTemplateResource { @@ -47,36 +45,11 @@ class GroovyHttpResonseStatusTemplateResource extends GroovyTemplateResource { groovyTemplateRenderer.handle(httpServerContext); } - @Override - public Path getPath() { - return null; - } - @Override public String getName() { return "status-resource"; } - @Override - public String getBaseName() { - return null; - } - - @Override - public String getSuffix() { - return null; - } - - @Override - public String getResourcePath() { - return null; - } - - @Override - public URL getURL() { - return null; - } - @Override public boolean isExists() { return true; diff --git a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResource.java b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResource.java index 604a7b7..cf8bb93 100644 --- a/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResource.java +++ b/net-http-template-groovy/src/main/java/org/xbib/net/http/template/groovy/GroovyTemplateResource.java @@ -17,9 +17,9 @@ import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -28,8 +28,16 @@ public class GroovyTemplateResource extends HtmlTemplateResource { private static final Logger logger = Logger.getLogger(GroovyTemplateResource.class.getName()); - private static final Map templates = new HashMap<>(); + /** + * This class might be reused by mtultiple handlers. + * We use a concurrent hash map here because in case of "not found" error templating, + * the "computeIfAbsent" can be called in a nested way, and would throw a concurrent modification exception. + */ + private static final Map templates = new ConcurrentHashMap<>(); + /** + * This lock ensures that changing locale and rendering is executed in a pairwisely manner. + */ private static final ReentrantLock lock = new ReentrantLock(); protected GroovyTemplateResource(HtmlTemplateResourceHandler templateResourceHandler, @@ -39,7 +47,7 @@ public class GroovyTemplateResource extends HtmlTemplateResource { @Override public void render(HttpServerContext httpServerContext) throws IOException { - logger.log(Level.FINE, "rendering groovy template, path = " + getPath() + " isExists = " + isExists() + " isDirectory =" + isDirectory() ); + logger.log(Level.FINER, "rendering groovy template, path = " + getPath() + " isExists = " + isExists() + " isDirectory =" + isDirectory() ); Application application = httpServerContext.attributes().get(Application.class, "application"); if (application == null) { logger.log(Level.WARNING, "application is null"); @@ -52,33 +60,35 @@ public class GroovyTemplateResource extends HtmlTemplateResource { } Path templatePath = getPath(); HttpService service = httpServerContext.attributes().get(HttpService.class, "service"); - GroovyTemplateService groovyTemplateService = (GroovyTemplateService) service; - if (groovyTemplateService.getTemplateName() != null) { - templatePath = application.resolve(groovyTemplateService.getTemplateName()); - logger.log(Level.FINE, "templatePath after application.resolve() = " + templatePath); - } else { - logger.log(Level.FINE, "the GroovyTemplateService does not have a template name set"); + if (service instanceof GroovyTemplateService groovyTemplateService) { + if (groovyTemplateService.getTemplateName() != null) { + templatePath = application.resolve(groovyTemplateService.getTemplateName()); + logger.log(Level.FINER, "templatePath after application.resolve() = " + templatePath); + } else { + logger.log(Level.FINER, "the GroovyTemplateService does not have a templateName"); + } } - logger.log(Level.FINE, "templatePath = " + templatePath); + // status response handlers have priority GroovyHttpResonseStatusTemplateResource resource = httpServerContext.attributes().get(GroovyHttpResonseStatusTemplateResource.class, "_resource"); if (resource != null) { - logger.log(Level.FINE, "Groovy HTTP status response rendering"); String indexFileName = resource.getIndexFileName(); if (indexFileName != null) { templatePath = application.resolve(indexFileName); } - } - // override if 'templatePath' attribute is set - String overridePath = httpServerContext.attributes().get(String.class, "templatePath"); - if (overridePath != null) { - logger.log(Level.FINE, "found override templatePath = " + overridePath); - templatePath = application.resolve(overridePath); - logger.log(Level.FINE, "found override templatePath, resolved to " + templatePath); - } - if (templatePath == null) { - logger.log(Level.FINE, "templatePath is null, OOTB effort on " + getIndexFileName()); - // OOTB rendering via getIndexFileName(), no getPath(), no getTemplateName() - templatePath = application.resolve(getIndexFileName()); + logger.log(Level.FINER, "rendering Groovy HTTP status response with templatePath = " + templatePath); + } else { + // override if 'templatePath' attribute is set + String overridePath = httpServerContext.attributes().get(String.class, "templatePath"); + if (overridePath != null) { + logger.log(Level.FINER, "found override templatePath = " + overridePath); + templatePath = application.resolve(overridePath); + logger.log(Level.FINER, "found override templatePath, resolved to " + templatePath); + } + if (templatePath == null) { + logger.log(Level.FINER, "templatePath is null, OOTB effort on " + getIndexFileName()); + // OOTB rendering via getIndexFileName(), no getPath(), no getTemplateName() + templatePath = application.resolve(getIndexFileName()); + } } if (isDirectory()) { if (isExistsIndexFile()) { @@ -88,11 +98,15 @@ public class GroovyTemplateResource extends HtmlTemplateResource { throw new HttpException("forbidden", httpServerContext, HttpResponseStatus.FORBIDDEN); } } - logger.log(Level.FINE, "rendering groovy template " + templatePath); + if (templatePath == null) { + logger.log(Level.WARNING, "unable to render a null path"); + throw new HttpException("internal path error", httpServerContext, HttpResponseStatus.INTERNAL_SERVER_ERROR); + } templates.computeIfAbsent(templatePath, path -> { try { + logger.log(Level.FINEST, "groovy templatePath = " + path + " creating by template engine"); return templateEngine.createTemplate(Files.readString(path)); - } catch (ClassNotFoundException | IOException e) { + } catch (Exception e) { throw new IllegalArgumentException(e); } }); @@ -104,35 +118,42 @@ public class GroovyTemplateResource extends HtmlTemplateResource { binding.setVariable("log", templateLogger); application.getModules().forEach(m -> binding.setVariable(m.getName(), m)); DefaultTemplateResolver templateResolver = httpServerContext.attributes().get(DefaultTemplateResolver.class, "templateresolver"); - if (templateResolver != null) { - // handle programmatic locale change plus template making under lock so no other request/response can interrupt us - logger.log(Level.FINER, "application locale for template = " + application.getLocale()); - try { - lock.lock(); - templateResolver.setLocale(application.getLocale()); - String acceptLanguage = httpServerContext.request().getHeaders().get(HttpHeaderNames.ACCEPT_LANGUAGE); - if (acceptLanguage != null) { - Locale negotiatedLocale = LocaleNegotiator.findLocale(acceptLanguage); - if (negotiatedLocale != null) { - logger.log(Level.FINER, "negotiated locale for template = " + negotiatedLocale); - templateResolver.setLocale(negotiatedLocale); - } - } - Writable writable = template.make(binding.getVariables()); - httpServerContext.attributes().put("writable", writable); - } catch (Exception e) { - // in case there is not template with negotiated locale - templateResolver.setLocale(application.getLocale()); - Writable writable = template.make(binding.getVariables()); - httpServerContext.attributes().put("writable", writable); - } finally { - lock.unlock(); - } - } else { - // for Groovy template engines without a resolver + if (templateResolver == null) { + // for Groovy template engines without a resolver, no need to set a locale Writable writable = template.make(binding.getVariables()); httpServerContext.attributes().put("writable", writable); + return; + } + if (!negotiateLocale) { + // if no locale negotiation configured, set always the applicaiton locale. This constant value never changes. + templateResolver.setLocale(application.getLocale()); + Writable writable = template.make(binding.getVariables()); + httpServerContext.attributes().put("writable", writable); + return; + } + // handle programmatic locale change plus template making under lock so no other request/response can interrupt us + logger.log(Level.FINER, "application locale for template = " + application.getLocale()); + try { + lock.lock(); + templateResolver.setLocale(application.getLocale()); + // language from request overrides application locale + String acceptLanguage = httpServerContext.request().getHeaders().get(HttpHeaderNames.ACCEPT_LANGUAGE); + if (acceptLanguage != null) { + Locale negotiatedLocale = LocaleNegotiator.findLocale(acceptLanguage); + if (negotiatedLocale != null) { + logger.log(Level.FINER, "negotiated locale for template = " + negotiatedLocale); + templateResolver.setLocale(negotiatedLocale); + } + } + Writable writable = template.make(binding.getVariables()); + httpServerContext.attributes().put("writable", writable); + } catch (Exception e) { + // fail silently by ignoring negotation + templateResolver.setLocale(application.getLocale()); + Writable writable = template.make(binding.getVariables()); + httpServerContext.attributes().put("writable", writable); + } finally { + lock.unlock(); } - logger.log(Level.FINER, "rendering done: " + httpServerContext.isDone()); } } diff --git a/settings.gradle b/settings.gradle index 164560c..27ab120 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,7 +8,7 @@ dependencyResolutionManagement { version('netty-tcnative', '2.0.59.Final') version('datastructures', '2.0.0') version('config', '5.0.2') - version('net', '3.0.3') + version('net', '3.0.4') library('junit-jupiter-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit') library('junit-jupiter-params', 'org.junit.jupiter', 'junit-jupiter-params').versionRef('junit') library('junit-jupiter-engine', 'org.junit.jupiter', 'junit-jupiter-engine').versionRef('junit')