working on form requests and Groovy templates, update to xbib net 3.0.4

This commit is contained in:
Jörg Prante 2023-03-21 16:41:49 +01:00
parent 5803e57072
commit 1ea725cbc2
15 changed files with 231 additions and 158 deletions

View file

@ -2,6 +2,8 @@ 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.http.cookie.SameSite;
import org.xbib.net.http.server.BaseApplication; import org.xbib.net.http.server.BaseApplication;
import org.xbib.net.http.server.HttpHandler; import org.xbib.net.http.server.HttpHandler;
import org.xbib.net.http.server.HttpServerContext; 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.OutgoingSessionHandler;
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.file.FileJsonSessionCodec;
import org.xbib.net.util.RandomUtil;
public class WebApplication extends BaseApplication { public class WebApplication extends BaseApplication {
@ -40,7 +43,8 @@ public class WebApplication extends BaseApplication {
sessionCodec, sessionCodec,
getStaticFileSuffixes(), getStaticFileSuffixes(),
"user_id", "user_id",
"e_user_id"); "e_user_id",
() -> RandomUtil.randomString(16));
} }
protected OutgoingSessionHandler buildOutgoingSessionHandler(HttpServerContext httpServerContext) { protected OutgoingSessionHandler buildOutgoingSessionHandler(HttpServerContext httpServerContext) {
@ -50,10 +54,13 @@ public class WebApplication extends BaseApplication {
getSecret(), getSecret(),
"HmacSHA1", "HmacSHA1",
sessionName, sessionName,
Duration.ofDays(1),
sessionCodec, sessionCodec,
getStaticFileSuffixes(), getStaticFileSuffixes(),
"user_id", "user_id",
"e_user_id"); "e_user_id",
Duration.ofDays(1),
true,
false,
SameSite.LAX);
} }
} }

View file

@ -9,6 +9,7 @@ import java.io.InputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects; import java.util.Objects;
public class HttpRequest extends BaseHttpRequest { public class HttpRequest extends BaseHttpRequest {
@ -47,7 +48,10 @@ public class HttpRequest extends BaseHttpRequest {
@Override @Override
public String toString() { 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() { public ByteBuf getByteBuf() {

View file

@ -2,6 +2,7 @@ package org.xbib.net.http.server.netty;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest;
import org.xbib.net.Parameter;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.net.http.HttpAddress; import org.xbib.net.http.HttpAddress;
import org.xbib.net.http.HttpMethod; import org.xbib.net.http.HttpMethod;
@ -24,7 +25,7 @@ public class HttpRequestBuilder extends BaseHttpRequestBuilder {
public HttpRequestBuilder setFullHttpRequest(FullHttpRequest fullHttpRequest) { public HttpRequestBuilder setFullHttpRequest(FullHttpRequest fullHttpRequest) {
if (fullHttpRequest != null) { 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(); this.fullHttpRequest = fullHttpRequest.retain();
setVersion(HttpVersion.valueOf(fullHttpRequest.protocolVersion().text())); setVersion(HttpVersion.valueOf(fullHttpRequest.protocolVersion().text()));
setMethod(HttpMethod.valueOf(fullHttpRequest.method().name())); setMethod(HttpMethod.valueOf(fullHttpRequest.method().name()));
@ -100,6 +101,10 @@ public class HttpRequestBuilder extends BaseHttpRequestBuilder {
return this; return this;
} }
protected Parameter getParameter() {
return super.parameter;
}
@Override @Override
public HttpRequest build() { public HttpRequest build() {
return new HttpRequest(this); return new HttpRequest(this);

View file

@ -1,5 +1,6 @@
package org.xbib.net.http.server; 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.server.route.HttpRouter;
import org.xbib.net.http.HttpAddress; import org.xbib.net.http.HttpAddress;
import org.xbib.net.http.HttpResponseStatus; 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.util.BlockingThreadPoolExecutor;
import org.xbib.net.http.server.validate.HttpRequestValidator; import org.xbib.net.http.server.validate.HttpRequestValidator;
import org.xbib.net.util.NamedThreadFactory; import org.xbib.net.util.NamedThreadFactory;
import org.xbib.net.util.RandomUtil;
import org.xbib.settings.Settings; import org.xbib.settings.Settings;
import java.io.Closeable; import java.io.Closeable;
@ -194,7 +196,8 @@ public class BaseApplication implements Application {
sessionCodec, sessionCodec,
getStaticFileSuffixes(), getStaticFileSuffixes(),
"user_id", "user_id",
"e_user_id"); "e_user_id",
() -> RandomUtil.randomString(16));
} }
protected HttpHandler buildOutgoingSessionHandler(HttpServerContext httpServerContext) { protected HttpHandler buildOutgoingSessionHandler(HttpServerContext httpServerContext) {
@ -204,11 +207,15 @@ public class BaseApplication implements Application {
getSecret(), getSecret(),
"HmacSHA1", "HmacSHA1",
sessionName, sessionName,
Duration.ofDays(1),
sessionCodec, sessionCodec,
getStaticFileSuffixes(), getStaticFileSuffixes(),
"user_id", "user_id",
"e_user_id"); "e_user_id",
Duration.ofDays(1),
true,
false,
SameSite.LAX
);
} }
protected HttpResponseRenderer buildResponseRenderer() { protected HttpResponseRenderer buildResponseRenderer() {

View file

@ -1,6 +1,5 @@
package org.xbib.net.http.server; package org.xbib.net.http.server;
import java.util.Map;
import org.xbib.net.Attributes; import org.xbib.net.Attributes;
import org.xbib.net.Parameter; import org.xbib.net.Parameter;
import org.xbib.net.ParameterBuilder; import org.xbib.net.ParameterBuilder;
@ -23,11 +22,15 @@ import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException; import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.xbib.net.http.HttpHeaderNames.CONTENT_TYPE; import static org.xbib.net.http.HttpHeaderNames.CONTENT_TYPE;
public class BaseHttpServerContext implements HttpServerContext { public class BaseHttpServerContext implements HttpServerContext {
private static final Logger logger = Logger.getLogger(BaseHttpServerContext.class.getName());
private static final String PATH_SEPARATOR = "/"; private static final String PATH_SEPARATOR = "/";
private final Application application; private final Application application;
@ -84,16 +87,26 @@ public class BaseHttpServerContext implements HttpServerContext {
@Override @Override
public void setResolverResult(HttpRouteResolver.Result<HttpService> pathResolverResult) { public void setResolverResult(HttpRouteResolver.Result<HttpService> pathResolverResult) {
this.pathResolverResult = pathResolverResult; this.pathResolverResult = pathResolverResult;
this.attributes.put("context", pathResolverResult.getContext()); if (pathResolverResult != null) {
this.attributes.put("handler", pathResolverResult.getValue()); attributes.put("context", pathResolverResult.getContext());
this.attributes.put("pathparams", pathResolverResult.getParameter()); attributes.put("handler", pathResolverResult.getValue());
String contextPath = pathResolverResult.getContext() != null ? attributes.put("pathparams", pathResolverResult.getParameter());
PATH_SEPARATOR + String.join(PATH_SEPARATOR, pathResolverResult.getContext()) : null; String contextPath = pathResolverResult.getContext() != null ?
setContextPath(contextPath); PATH_SEPARATOR + String.join(PATH_SEPARATOR, pathResolverResult.getContext()) : null;
setContextURL(request().getBaseURL().resolve(contextPath != null ? contextPath + "/" : "")); setContextPath(contextPath);
this.httpRequest = createRequest(httpRequestBuilder); setContextURL(request().getBaseURL().resolve(contextPath != null ? contextPath + "/" : ""));
this.attributes.put("request", httpRequest); } else {
this.next = false; // 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) { public void setContextPath(String contextPath) {
@ -114,6 +127,7 @@ public class BaseHttpServerContext implements HttpServerContext {
return contextURL; return contextURL;
} }
@Override
public Path resolve(String path) { public Path resolve(String path) {
return application.resolve(path); return application.resolve(path);
} }
@ -187,8 +201,8 @@ public class BaseHttpServerContext implements HttpServerContext {
httpResponseBuilder.write(fileChannel, bufferSize); httpResponseBuilder.write(fileChannel, bufferSize);
} }
protected HttpRequest createRequest(HttpRequestBuilder requestBuilder) { protected HttpRequest createRequest() {
HttpHeaders headers = requestBuilder.getHeaders(); HttpHeaders headers = httpRequestBuilder.getHeaders();
String mimeType = headers.get(CONTENT_TYPE); String mimeType = headers.get(CONTENT_TYPE);
Charset charset = StandardCharsets.UTF_8; Charset charset = StandardCharsets.UTF_8;
if (mimeType != null) { if (mimeType != null) {
@ -198,30 +212,40 @@ public class BaseHttpServerContext implements HttpServerContext {
// helper URL to collect parameters in request URI // helper URL to collect parameters in request URI
URL url = URL.builder() URL url = URL.builder()
.charset(charset, CodingErrorAction.REPLACE) .charset(charset, CodingErrorAction.REPLACE)
.path(requestBuilder.getRequestURI()) .path(httpRequestBuilder.getRequestURI())
.build(); .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 // 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))) { (mimeType != null && mimeType.contains(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED))) {
Charset htmlCharset = getCharset(mimeType, StandardCharsets.ISO_8859_1); Charset htmlCharset = getCharset(mimeType, StandardCharsets.ISO_8859_1);
CharBuffer charBuffer = requestBuilder.getBodyAsChars(htmlCharset); CharBuffer charBuffer = httpRequestBuilder.getBodyAsChars(htmlCharset);
if (charBuffer != null) { if (charBuffer != null) {
formParameters.addPercentEncodedBody(charBuffer.toString()); formParameterBuilder.addPercentEncodedBody(charBuffer.toString());
} }
} }
CookieBox cookieBox = attributes.get(CookieBox.class, "incomingcookies"); CookieBox cookieBox = attributes.get(CookieBox.class, "incomingcookies");
ParameterBuilder cookieParameters = Parameter.builder().domain("COOKIE"); ParameterBuilder cookieParameterBuilder = Parameter.builder().domain("COOKIE");
if (cookieBox != null) { 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()); Parameter queryParameter = url.getQueryParams();
parameterBuilder.add(formParameters.build()); logger.log(Level.FINER, "adding query parameters = " + queryParameter.getDomain() + " " + queryParameter.allToString());
parameterBuilder.add(cookieParameters.build()); parameterBuilder.add(queryParameter);
parameterBuilder.add(pathResolverResult.getParameter()); Parameter formParameter = formParameterBuilder.build();
requestBuilder.setParameter(parameterBuilder.build()); logger.log(Level.FINER, "adding form parameters = " + formParameter.getDomain() + " " + formParameter.allToString());
requestBuilder.setContext(this); parameterBuilder.add(formParameter);
return requestBuilder.build(); 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) { private static Charset getCharset(String contentTypeValue, Charset defaultCharset) {

View file

@ -39,34 +39,37 @@ public class HtmlTemplateResource implements HttpServerResource {
private final String suffix; private final String suffix;
protected final boolean negotiateLocale;
protected HtmlTemplateResource(HtmlTemplateResourceHandler templateResourceHandler, protected HtmlTemplateResource(HtmlTemplateResourceHandler templateResourceHandler,
HttpServerContext httpServerContext) throws IOException { HttpServerContext httpServerContext) throws IOException {
this.templateResourceHandler = templateResourceHandler; this.templateResourceHandler = templateResourceHandler;
String indexFileName = templateResourceHandler.getIndexFileName(); String indexFileName = templateResourceHandler.getIndexFileName();
Application application = httpServerContext.attributes().get(Application.class, "application"); Application application = httpServerContext.attributes().get(Application.class, "application");
this.negotiateLocale = application.getSettings().getAsBoolean("negotiateLocale", false);
Path root = templateResourceHandler.getPrefix(); Path root = templateResourceHandler.getPrefix();
root = root != null ? root : application.getHome(); root = root != null ? root : application.getHome();
if (root == null) { if (root == null) {
throw new IllegalArgumentException("no home path set for template resource resolving"); 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); this.resourcePath = httpServerContext.request().getRequestPath().substring(1);
logger.log(Level.FINE, "resource path = " + resourcePath);
this.path = resourcePath.length() > 0 ? root.resolve(resourcePath) : root; this.path = resourcePath.length() > 0 ? root.resolve(resourcePath) : root;
logger.log(Level.FINE, "path = " + path); logger.log(Level.FINER, "class = " + getClass().getName() +
logger.log(Level.FINE, "index file name = " + indexFileName); " root = " + root +
" resource path = " + resourcePath +
" path = " + path +
" index file name = " + indexFileName);
this.name = path.getFileName().toString(); this.name = path.getFileName().toString();
this.baseName = AbstractResourceHandler.basename(name); this.baseName = AbstractResourceHandler.basename(name);
this.suffix = AbstractResourceHandler.suffix(name); this.suffix = AbstractResourceHandler.suffix(name);
if (Files.isDirectory(path)) { if (Files.isDirectory(path)) {
if (getIndexFileName() != null) { if (getIndexFileName() != null) {
Path indexPath = path.resolve(indexFileName); 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)) { if (Files.exists(indexPath)) {
logger.log(Level.FINE, "index path exists");
this.isExistsIndexFile = true; this.isExistsIndexFile = true;
this.path = indexPath; this.path = indexPath;
logger.log(Level.FINE, "index file path found = " + path);
this.isDirectory = false; this.isDirectory = false;
} else { } else {
this.isExistsIndexFile = false; this.isExistsIndexFile = false;
@ -81,15 +84,9 @@ public class HtmlTemplateResource implements HttpServerResource {
this.isDirectory = false; this.isDirectory = false;
} }
this.isExists = Files.exists(path); this.isExists = Files.exists(path);
logger.log(Level.FINE, "exists = " + isExists);
logger.log(Level.FINE, "isDirectory = " + isDirectory);
this.url = URL.create(path.toUri().toString()); this.url = URL.create(path.toUri().toString());
logger.log(Level.FINE, "url = " + url); logger.log(Level.FINE, "isExists = " + isExists + " isDirectory = " + isDirectory + " url = " + url);
if (isExists) { this.lastModified = isExists ? Files.getLastModifiedTime(path).toInstant() : Instant.now();
this.lastModified = Files.getLastModifiedTime(path).toInstant();
} else {
this.lastModified = Instant.now();
}
// length will be computed at rendering time // length will be computed at rendering time
this.length = -1; this.length = -1;
} }

View file

@ -63,10 +63,9 @@ public class BaseHttpRoute implements HttpRoute {
@Override @Override
public boolean matches(ParameterBuilder parameterBuilder, HttpRoute requestedRoute) { public boolean matches(ParameterBuilder parameterBuilder, HttpRoute requestedRoute) {
if (!(requestedRoute instanceof BaseHttpRoute)) { if (!(requestedRoute instanceof BaseHttpRoute baseHttpRoute)) {
return false; return false;
} }
BaseHttpRoute baseHttpRoute = (BaseHttpRoute) requestedRoute;
if (!httpAddress.equals(baseHttpRoute.getHttpAddress())) { if (!httpAddress.equals(baseHttpRoute.getHttpAddress())) {
return false; return false;
} }

View file

@ -29,7 +29,7 @@ public class BaseHttpRouteResolver<T> implements HttpRouteResolver<T> {
@Override @Override
public void resolve(HttpRoute httpRoute, ResultListener<T> listener) { public void resolve(HttpRoute httpRoute, ResultListener<T> listener) {
for (Map.Entry<HttpRoute, T> entry : builder.routes) { for (Map.Entry<HttpRoute, T> entry : builder.routes) {
ParameterBuilder parameterBuilder = Parameter.builder(); ParameterBuilder parameterBuilder = Parameter.builder().domain("PATH");
boolean match = entry.getKey().matches(parameterBuilder, httpRoute); boolean match = entry.getKey().matches(parameterBuilder, httpRoute);
if (match && listener != null) { if (match && listener != null) {
List<String> list = Arrays.stream(httpRoute.getPath().replaceFirst(builder.prefix, "").split("/")) List<String> list = Arrays.stream(httpRoute.getPath().replaceFirst(builder.prefix, "").split("/"))

View file

@ -20,10 +20,12 @@ import org.xbib.net.http.server.Application;
import org.xbib.net.http.server.HttpDomain; import org.xbib.net.http.server.HttpDomain;
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.HttpRequest;
import org.xbib.net.http.server.HttpRequestBuilder; import org.xbib.net.http.server.HttpRequestBuilder;
import org.xbib.net.http.server.HttpResponseBuilder; import org.xbib.net.http.server.HttpResponseBuilder;
import org.xbib.net.http.server.HttpServerContext; import org.xbib.net.http.server.HttpServerContext;
import org.xbib.net.http.server.HttpService; import org.xbib.net.http.server.HttpService;
import org.xbib.net.http.server.handler.InternalServerErrorHandler;
import static org.xbib.net.http.HttpResponseStatus.NOT_FOUND; import static org.xbib.net.http.HttpResponseStatus.NOT_FOUND;
@ -105,6 +107,7 @@ public class BaseHttpRouter implements HttpRouter {
} }
if (httpRouteResolverResults.isEmpty()) { if (httpRouteResolverResults.isEmpty()) {
logger.log(Level.FINE, "route resolver results is empty, generating a not found message"); logger.log(Level.FINE, "route resolver results is empty, generating a not found message");
httpServerContext.setResolverResult(null);
routeStatus(NOT_FOUND, httpServerContext); routeStatus(NOT_FOUND, httpServerContext);
return; return;
} }
@ -113,7 +116,8 @@ public class BaseHttpRouter implements HttpRouter {
// first: create the final request // first: create the final request
httpServerContext.setResolverResult(httpRouteResolverResult); httpServerContext.setResolverResult(httpRouteResolverResult);
HttpService httpService = httpRouteResolverResult.getValue(); 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. // second: security check, authentication etc.
if (httpService.getSecurityDomain() != null) { if (httpService.getSecurityDomain() != null) {
logger.log(Level.FINEST, () -> "handling security domain service " + httpService); logger.log(Level.FINEST, () -> "handling security domain service " + httpService);
@ -125,11 +129,12 @@ public class BaseHttpRouter implements HttpRouter {
if (httpServerContext.isDone() || httpServerContext.isFailed()) { if (httpServerContext.isDone() || httpServerContext.isFailed()) {
break; break;
} }
// accept service and execute service // after security checks, accept service, open and execute service
httpServerContext.attributes().put("service", httpService); httpServerContext.attributes().put("service", httpService);
application.getModules().forEach(module -> module.onOpen(application, httpServerContext, httpService)); application.getModules().forEach(module -> module.onOpen(application, httpServerContext, httpService));
logger.log(Level.FINEST, () -> "handling service " + httpService); logger.log(Level.FINEST, () -> "handling service " + httpService);
httpService.handle(httpServerContext); httpService.handle(httpServerContext);
// if service signals that work is done, break
if (httpServerContext.isDone() || httpServerContext.isFailed()) { if (httpServerContext.isDone() || httpServerContext.isFailed()) {
break; break;
} }
@ -155,10 +160,17 @@ public class BaseHttpRouter implements HttpRouter {
@Override @Override
public void routeStatus(HttpResponseStatus httpResponseStatus, HttpServerContext httpServerContext) { public void routeStatus(HttpResponseStatus httpResponseStatus, HttpServerContext httpServerContext) {
logger.log(Level.FINER, "routing status " + httpResponseStatus);
try { try {
HttpHandler httpHandler = getHandler(httpResponseStatus); 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(); httpServerContext.response().reset();
httpHandler.handle(httpServerContext); httpHandler.handle(httpServerContext);
httpServerContext.done();
logger.log(Level.FINER, "routing status " + httpResponseStatus + " done");
} catch (IOException ioe) { } catch (IOException ioe) {
throw new IllegalStateException("unable to route response status, reason: " + ioe.getMessage(), ioe); throw new IllegalStateException("unable to route response status, reason: " + ioe.getMessage(), ioe);
} }

View file

@ -21,9 +21,9 @@ import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.xbib.net.util.RandomUtil;
public class IncomingSessionHandler implements HttpHandler { public class IncomingSessionHandler implements HttpHandler {
@ -46,13 +46,16 @@ public class IncomingSessionHandler implements HttpHandler {
*/ */
private final Set<String> suffixes; private final Set<String> suffixes;
Supplier<String> sessionIdGenerator;
public IncomingSessionHandler(String sessionSecret, public IncomingSessionHandler(String sessionSecret,
String sessionCookieAlgorithm, String sessionCookieAlgorithm,
String sessionCookieName, String sessionCookieName,
Codec<Session> sessionCodec, Codec<Session> sessionCodec,
Set<String> suffixes, Set<String> suffixes,
String sessionUserName, String sessionUserName,
String sessionEffectiveUserName) { String sessionEffectiveUserName,
Supplier<String> sessionIdGenerator) {
this.sessionSecret = sessionSecret; this.sessionSecret = sessionSecret;
this.sessionCookieAlgorithm = sessionCookieAlgorithm; this.sessionCookieAlgorithm = sessionCookieAlgorithm;
this.sessionCookieName = sessionCookieName; this.sessionCookieName = sessionCookieName;
@ -60,6 +63,7 @@ public class IncomingSessionHandler implements HttpHandler {
this.suffixes = suffixes; this.suffixes = suffixes;
this.sessionUserName = sessionUserName; this.sessionUserName = sessionUserName;
this.sessionEffectiveUserName = sessionEffectiveUserName; this.sessionEffectiveUserName = sessionEffectiveUserName;
this.sessionIdGenerator = sessionIdGenerator;
} }
@Override @Override
@ -76,6 +80,7 @@ public class IncomingSessionHandler implements HttpHandler {
if (session == null) { if (session == null) {
try { try {
Map<String, Object> payload = decodeCookie(cookie); Map<String, Object> payload = decodeCookie(cookie);
logger.log(Level.FINER, "cookie decoded");
session = toSession(payload); session = toSession(payload);
UserProfile userProfile = newUserProfile(payload, session); UserProfile userProfile = newUserProfile(payload, session);
if (userProfile != null) { if (userProfile != null) {
@ -96,14 +101,14 @@ public class IncomingSessionHandler implements HttpHandler {
} }
if (session == null) { if (session == null) {
try { try {
session = sessionCodec.create(RandomUtil.randomString(32)); session = sessionCodec.create(sessionIdGenerator.get());
} catch (IOException e) { } catch (IOException 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 session", context, HttpResponseStatus.INTERNAL_SERVER_ERROR);
} }
} }
context.attributes().put("session", session); context.attributes().put("session", session);
logger.log(Level.FINER, "incoming session " + session.id());
} }
private Map<String, Object> decodeCookie(Cookie cookie) throws IOException, private Map<String, Object> decodeCookie(Cookie cookie) throws IOException,

View file

@ -47,22 +47,34 @@ public class OutgoingSessionHandler implements HttpHandler {
private final String sessionEffectiveUserName; private final String sessionEffectiveUserName;
private final boolean httpOnly;
private final boolean secure;
private final SameSite sameSite;
public OutgoingSessionHandler(String sessionSecret, public OutgoingSessionHandler(String sessionSecret,
String sessionCookieAlgorithm, String sessionCookieAlgorithm,
String sessionCookieName, String sessionCookieName,
Duration sessionDuration,
Codec<Session> sessionCodec, Codec<Session> sessionCodec,
Set<String> suffixes, Set<String> suffixes,
String sessionUserName, String sessionUserName,
String sessionEffectiveUserName) { String sessionEffectiveUserName,
Duration sessionDuration,
boolean httpOnly,
boolean secure,
SameSite sameSite) {
this.sessionSecret = sessionSecret; this.sessionSecret = sessionSecret;
this.sessionCookieAlgorithm = sessionCookieAlgorithm; this.sessionCookieAlgorithm = sessionCookieAlgorithm;
this.sessionCookieName = sessionCookieName; this.sessionCookieName = sessionCookieName;
this.sessionDuration = sessionDuration;
this.sessionCodec = sessionCodec; this.sessionCodec = sessionCodec;
this.suffixes = suffixes; this.suffixes = suffixes;
this.sessionUserName = sessionUserName; this.sessionUserName = sessionUserName;
this.sessionEffectiveUserName = sessionEffectiveUserName; this.sessionEffectiveUserName = sessionEffectiveUserName;
this.sessionDuration = sessionDuration;
this.httpOnly = httpOnly;
this.secure = secure;
this.sameSite = sameSite;
} }
@Override @Override
@ -137,28 +149,32 @@ public class OutgoingSessionHandler implements HttpHandler {
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 ("localhost".equals(domain)) {
logger.log(Level.WARNING, "localhost not set as a cookie domain");
} else {
cookie.setDomain('.' + domain); cookie.setDomain('.' + domain);
} }
cookie.setPath(path); cookie.setPath(path);
cookie.setMaxAge(sessionDuration.toSeconds()); cookie.setMaxAge(sessionDuration.toSeconds());
cookie.setHttpOnly(true); cookie.setHttpOnly(httpOnly);
cookie.setSecure(true); cookie.setSecure(secure);
cookie.setSameSite(SameSite.LAX); cookie.setSameSite(sameSite);
return cookie; return cookie;
} }
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 ("localhost".equals(domain)) {
logger.log(Level.WARNING, "localhost not set as a cookie domain");
} else {
cookie.setDomain('.' + domain); cookie.setDomain('.' + domain);
} }
cookie.setPath(path); cookie.setPath(path);
cookie.setMaxAge(0L); cookie.setMaxAge(0L);
cookie.setHttpOnly(true); cookie.setHttpOnly(httpOnly);
cookie.setSecure(true); cookie.setSecure(secure);
cookie.setSameSite(SameSite.LAX); cookie.setSameSite(sameSite);
logger.log(Level.FINEST, "baked empty cookie"); logger.log(Level.FINEST, "baked empty cookie");
return cookie; return cookie;
} }

View file

@ -44,28 +44,31 @@ public abstract class DefaultMarkupTemplate extends BaseTemplate {
super(templateEngine, model, modelTypes, configuration); super(templateEngine, model, modelTypes, configuration);
this.application = (Application) model.get("application"); this.application = (Application) model.get("application");
Objects.requireNonNull(this.application, "application must not be null"); 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"); this.responseBuilder = (HttpResponseBuilder) model.get("responsebuilder");
Objects.requireNonNull(this.responseBuilder, "response must not be null"); 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); responseBuilder.setResponseStatus(responseStatus);
} }
public void contentType(String contentType) { public void setContentType(String contentType) {
responseBuilder.setHeader(HttpHeaderNames.CONTENT_TYPE, contentType); responseBuilder.setHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
} }
public boolean isContentType(String 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); request.getHeaders().get(HttpHeaderNames.CONTENT_TYPE).startsWith(contentType);
} }
public void contentDisposition(String contentDisposition) { public void setContentDisposition(String contentDisposition) {
responseBuilder.setHeader(HttpHeaderNames.CONTENT_DISPOSITION, contentDisposition); responseBuilder.setHeader(HttpHeaderNames.CONTENT_DISPOSITION, contentDisposition);
} }

View file

@ -2,13 +2,11 @@ package org.xbib.net.http.template.groovy;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.xbib.net.URL;
import org.xbib.net.http.HttpResponseStatus; import org.xbib.net.http.HttpResponseStatus;
import org.xbib.net.http.server.Application; import org.xbib.net.http.server.Application;
import org.xbib.net.http.server.HttpServerContext; import org.xbib.net.http.server.HttpServerContext;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
class GroovyHttpResonseStatusTemplateResource extends GroovyTemplateResource { class GroovyHttpResonseStatusTemplateResource extends GroovyTemplateResource {
@ -47,36 +45,11 @@ class GroovyHttpResonseStatusTemplateResource extends GroovyTemplateResource {
groovyTemplateRenderer.handle(httpServerContext); groovyTemplateRenderer.handle(httpServerContext);
} }
@Override
public Path getPath() {
return null;
}
@Override @Override
public String getName() { public String getName() {
return "status-resource"; 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 @Override
public boolean isExists() { public boolean isExists() {
return true; return true;

View file

@ -17,9 +17,9 @@ import org.xbib.net.http.server.resource.HtmlTemplateResourceHandler;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; 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 Logger logger = Logger.getLogger(GroovyTemplateResource.class.getName());
private static final Map<Path, Template> 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<Path, Template> templates = new ConcurrentHashMap<>();
/**
* This lock ensures that changing locale and rendering is executed in a pairwisely manner.
*/
private static final ReentrantLock lock = new ReentrantLock(); private static final ReentrantLock lock = new ReentrantLock();
protected GroovyTemplateResource(HtmlTemplateResourceHandler templateResourceHandler, protected GroovyTemplateResource(HtmlTemplateResourceHandler templateResourceHandler,
@ -39,7 +47,7 @@ public class GroovyTemplateResource extends HtmlTemplateResource {
@Override @Override
public void render(HttpServerContext httpServerContext) throws IOException { 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"); Application application = httpServerContext.attributes().get(Application.class, "application");
if (application == null) { if (application == null) {
logger.log(Level.WARNING, "application is null"); logger.log(Level.WARNING, "application is null");
@ -52,33 +60,35 @@ public class GroovyTemplateResource extends HtmlTemplateResource {
} }
Path templatePath = getPath(); Path templatePath = getPath();
HttpService service = httpServerContext.attributes().get(HttpService.class, "service"); HttpService service = httpServerContext.attributes().get(HttpService.class, "service");
GroovyTemplateService groovyTemplateService = (GroovyTemplateService) service; if (service instanceof GroovyTemplateService groovyTemplateService) {
if (groovyTemplateService.getTemplateName() != null) { if (groovyTemplateService.getTemplateName() != null) {
templatePath = application.resolve(groovyTemplateService.getTemplateName()); templatePath = application.resolve(groovyTemplateService.getTemplateName());
logger.log(Level.FINE, "templatePath after application.resolve() = " + templatePath); logger.log(Level.FINER, "templatePath after application.resolve() = " + templatePath);
} else { } else {
logger.log(Level.FINE, "the GroovyTemplateService does not have a template name set"); 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"); GroovyHttpResonseStatusTemplateResource resource = httpServerContext.attributes().get(GroovyHttpResonseStatusTemplateResource.class, "_resource");
if (resource != null) { if (resource != null) {
logger.log(Level.FINE, "Groovy HTTP status response rendering");
String indexFileName = resource.getIndexFileName(); String indexFileName = resource.getIndexFileName();
if (indexFileName != null) { if (indexFileName != null) {
templatePath = application.resolve(indexFileName); templatePath = application.resolve(indexFileName);
} }
} logger.log(Level.FINER, "rendering Groovy HTTP status response with templatePath = " + templatePath);
// override if 'templatePath' attribute is set } else {
String overridePath = httpServerContext.attributes().get(String.class, "templatePath"); // override if 'templatePath' attribute is set
if (overridePath != null) { String overridePath = httpServerContext.attributes().get(String.class, "templatePath");
logger.log(Level.FINE, "found override templatePath = " + overridePath); if (overridePath != null) {
templatePath = application.resolve(overridePath); logger.log(Level.FINER, "found override templatePath = " + overridePath);
logger.log(Level.FINE, "found override templatePath, resolved to " + templatePath); templatePath = application.resolve(overridePath);
} logger.log(Level.FINER, "found override templatePath, resolved to " + templatePath);
if (templatePath == null) { }
logger.log(Level.FINE, "templatePath is null, OOTB effort on " + getIndexFileName()); if (templatePath == null) {
// OOTB rendering via getIndexFileName(), no getPath(), no getTemplateName() logger.log(Level.FINER, "templatePath is null, OOTB effort on " + getIndexFileName());
templatePath = application.resolve(getIndexFileName()); // OOTB rendering via getIndexFileName(), no getPath(), no getTemplateName()
templatePath = application.resolve(getIndexFileName());
}
} }
if (isDirectory()) { if (isDirectory()) {
if (isExistsIndexFile()) { if (isExistsIndexFile()) {
@ -88,11 +98,15 @@ public class GroovyTemplateResource extends HtmlTemplateResource {
throw new HttpException("forbidden", httpServerContext, HttpResponseStatus.FORBIDDEN); 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 -> { templates.computeIfAbsent(templatePath, path -> {
try { try {
logger.log(Level.FINEST, "groovy templatePath = " + path + " creating by template engine");
return templateEngine.createTemplate(Files.readString(path)); return templateEngine.createTemplate(Files.readString(path));
} catch (ClassNotFoundException | IOException e) { } catch (Exception e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
}); });
@ -104,35 +118,42 @@ public class GroovyTemplateResource extends HtmlTemplateResource {
binding.setVariable("log", templateLogger); binding.setVariable("log", templateLogger);
application.getModules().forEach(m -> binding.setVariable(m.getName(), m)); application.getModules().forEach(m -> binding.setVariable(m.getName(), m));
DefaultTemplateResolver templateResolver = httpServerContext.attributes().get(DefaultTemplateResolver.class, "templateresolver"); DefaultTemplateResolver templateResolver = httpServerContext.attributes().get(DefaultTemplateResolver.class, "templateresolver");
if (templateResolver != null) { if (templateResolver == null) {
// handle programmatic locale change plus template making under lock so no other request/response can interrupt us // for Groovy template engines without a resolver, no need to set a locale
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
Writable writable = template.make(binding.getVariables()); Writable writable = template.make(binding.getVariables());
httpServerContext.attributes().put("writable", writable); 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());
} }
} }

View file

@ -8,7 +8,7 @@ dependencyResolutionManagement {
version('netty-tcnative', '2.0.59.Final') version('netty-tcnative', '2.0.59.Final')
version('datastructures', '2.0.0') version('datastructures', '2.0.0')
version('config', '5.0.2') 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-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit')
library('junit-jupiter-params', 'org.junit.jupiter', 'junit-jupiter-params').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') library('junit-jupiter-engine', 'org.junit.jupiter', 'junit-jupiter-engine').versionRef('junit')