diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java index 92971c63..b0ed8bce 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java @@ -77,8 +77,8 @@ public class QApplicationJavalinServer private boolean serveLegacyUnversionedMiddlewareAPI = true; private List middlewareVersionList = List.of(new MiddlewareVersionV1()); private List additionalRouteProviders = null; - private Consumer javalinConfigurationCustomizer = null; - private QJavalinMetaData javalinMetaData = null; + private Consumer javalinConfigurationCustomizer = null; + private QJavalinMetaData javalinMetaData = null; private long lastQInstanceHotSwapMillis; private long millisBetweenHotSwaps = 2500; @@ -197,6 +197,15 @@ public class QApplicationJavalinServer } }); + ////////////////////////////////////////////////////////////////////// + // also pass the javalin service into any additionalRouteProviders, // + // in case they need additional setup, e.g., before/after handlers. // + ////////////////////////////////////////////////////////////////////// + for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders)) + { + routeProvider.acceptJavalinService(service); + } + ////////////////////////////////////////////////////////////////////////////////////// // per system property, set the server to hot-swap the q instance before all routes // ////////////////////////////////////////////////////////////////////////////////////// @@ -228,7 +237,7 @@ public class QApplicationJavalinServer /*************************************************************************** ** initial tests with the SimpleFileSystemDirectoryRouter would sometimes ** have a Content-Type:text/html;charset=null ! - ** which doesn't seem every valid (and at least it broke our unit test). + ** which doesn't seem ever valid (and at least it broke our unit test). ** so, if w see charset=null in contentType, replace it with the system ** default, which may not be 100% right, but has to be better than "null"... ***************************************************************************/ @@ -242,7 +251,6 @@ public class QApplicationJavalinServer contentType = contentType.replace("charset=null", "charset=" + Charset.defaultCharset().name()); context.res().setContentType(contentType); } - System.out.println(); }); } @@ -630,6 +638,7 @@ public class QApplicationJavalinServer } + /******************************************************************************* ** Getter for javalinMetaData *******************************************************************************/ @@ -659,5 +668,4 @@ public class QApplicationJavalinServer return (this); } - } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java index 41784503..c6dbf40b 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.middleware.javalin; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import io.javalin.Javalin; import io.javalin.apibuilder.EndpointGroup; import io.javalin.config.JavalinConfig; @@ -53,7 +54,9 @@ public interface QJavalinRouteProviderInterface /*************************************************************************** - ** + ** when the javalin service is being configured as part of its boot up, + ** accept the javalinConfig object, to perform whatever setup you need, + ** such as setting up routes. ***************************************************************************/ default void acceptJavalinConfig(JavalinConfig config) { @@ -61,4 +64,17 @@ public interface QJavalinRouteProviderInterface // noop at default // ///////////////////// } + + /*************************************************************************** + ** when the javalin service is being configured as part of its boot up, + ** accept the Javalin service object, to perform whatever setup you need, + ** such as setting up before/after handlers. + ***************************************************************************/ + default void acceptJavalinService(Javalin service) + { + ///////////////////// + // noop at default // + ///////////////////// + } + } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java index 3b0e50c6..82c09173 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.middleware.javalin.metadata; import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; /******************************************************************************* @@ -38,6 +39,8 @@ public class JavalinRouteProviderMetaData implements QMetaDataObject private List methods; + private QCodeReference routeAuthenticator; + /******************************************************************************* @@ -172,4 +175,35 @@ public class JavalinRouteProviderMetaData implements QMetaDataObject return (this); } + + + /******************************************************************************* + ** Getter for routeAuthenticator + *******************************************************************************/ + public QCodeReference getRouteAuthenticator() + { + return (this.routeAuthenticator); + } + + + + /******************************************************************************* + ** Setter for routeAuthenticator + *******************************************************************************/ + public void setRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + } + + + + /******************************************************************************* + ** Fluent setter for routeAuthenticator + *******************************************************************************/ + public JavalinRouteProviderMetaData withRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + return (this); + } + } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java index 00ae3abc..10ff7b5d 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java @@ -27,22 +27,31 @@ import java.io.Serializable; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +import com.kingsrook.qqq.backend.core.actions.tables.StorageAction; import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import com.kingsrook.qqq.backend.javalin.QJavalinUtils; import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface; import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; +import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface; import io.javalin.apibuilder.ApiBuilder; import io.javalin.apibuilder.EndpointGroup; import io.javalin.http.Context; import io.javalin.http.HttpStatus; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -55,7 +64,10 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface private final String hostedPath; private final String processName; private final List methods; - private QInstance qInstance; + + private QCodeReference routeAuthenticator; + + private QInstance qInstance; @@ -76,6 +88,7 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface public ProcessBasedRouter(JavalinRouteProviderMetaData routeProvider) { this(routeProvider.getHostedPath(), routeProvider.getProcessName(), routeProvider.getMethods()); + setRouteAuthenticator(routeProvider.getRouteAuthenticator()); } @@ -145,41 +158,36 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface RunProcessInput input = new RunProcessInput(); input.setProcessName(processName); - try + QContext.init(qInstance, new QSystemUserSession()); + + boolean isAuthenticated = false; + if(routeAuthenticator == null) { - QJavalinImplementation.setupSession(context, input); + isAuthenticated = true; } - catch(Exception e) + else { - context.header("WWW-Authenticate", "Basic realm=\"Access to this QQQ site\""); - context.status(HttpStatus.UNAUTHORIZED); + try + { + RouteAuthenticatorInterface routeAuthenticator = QCodeLoader.getAdHoc(RouteAuthenticatorInterface.class, this.routeAuthenticator); + isAuthenticated = routeAuthenticator.authenticateRequest(context); + } + catch(Exception e) + { + context.skipRemainingHandlers(); + QJavalinImplementation.handleException(context, e); + } + } + + if(!isAuthenticated) + { + LOG.info("Request is not authenticated, so returning before running process", logPair("processName", processName), logPair("path", context.path())); return; } - /* - boolean authorized = false; - String authorization = context.header("Authorization"); - if(authorization != null && authorization.matches("^Basic .+")) - { - String base64Authorization = authorization.substring("Basic ".length()); - String decoded = new String(Base64.getDecoder().decode(base64Authorization), StandardCharsets.UTF_8); - String[] parts = decoded.split(":", 2); - - QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); - QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication()); - } - - if(!authorized) - { - } - - // todo - not always system-user session!! - QContext.init(this.qInstance, new QSystemUserSession()); - */ - try { - LOG.info("Running [" + processName + "] to serve [" + context.path() + "]..."); + LOG.info("Running process to serve route", logPair("processName", processName), logPair("path", context.path())); ///////////////////// // run the process // @@ -190,16 +198,10 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface input.addValue("pathParams", new HashMap<>(context.pathParamMap())); input.addValue("queryParams", new HashMap<>(context.queryParamMap())); input.addValue("formParams", new HashMap<>(context.formParamMap())); - RunProcessOutput runProcessOutput = new RunProcessAction().execute(input); + input.addValue("cookies", new HashMap<>(context.cookieMap())); + input.addValue("requestHeaders", new HashMap<>(context.headerMap())); - ///////////////// - // status code // - ///////////////// - Integer statusCode = runProcessOutput.getValueInteger("statusCode"); - if(statusCode != null) - { - context.status(statusCode); - } + RunProcessOutput runProcessOutput = new RunProcessAction().execute(input); ///////////////// // headers map // @@ -217,26 +219,46 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface // maybe via the callback object??? input.setCallback(new QProcessCallback() {}); // context.resultInputStream(); - /////////////////// - // response body // - /////////////////// - Serializable response = runProcessOutput.getValue("response"); - if(response instanceof String s) + ////////////// + // response // + ////////////// + Integer statusCode = runProcessOutput.getValueInteger("statusCode"); + String redirectURL = runProcessOutput.getValueString("redirectURL"); + String responseString = runProcessOutput.getValueString("responseString"); + byte[] responseBytes = runProcessOutput.getValueByteArray("responseBytes"); + StorageInput responseStorageInput = (StorageInput) runProcessOutput.getValue("responseStorageInput"); + + if(StringUtils.hasContent(redirectURL)) { - context.result(s); + context.redirect(redirectURL, statusCode == null ? HttpStatus.FOUND : HttpStatus.forStatus(statusCode)); + return; } - else if(response instanceof byte[] ba) + + if(statusCode != null) { - context.result(ba); + context.status(statusCode); } - else if(response instanceof InputStream is) + + if(StringUtils.hasContent(responseString)) { - context.result(is); + context.result(responseString); + return; } - else + + if(responseBytes != null && responseBytes.length > 0) { - context.result(ValueUtils.getValueAsString(response)); + context.result(responseBytes); + return; } + + if(responseStorageInput != null) + { + InputStream inputStream = new StorageAction().getInputStream(responseStorageInput); + context.result(inputStream); + return; + } + + throw (new QException("No response value was set in the process output state.")); } catch(Exception e) { @@ -248,4 +270,35 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface } } + + + /******************************************************************************* + ** Getter for routeAuthenticator + *******************************************************************************/ + public QCodeReference getRouteAuthenticator() + { + return (this.routeAuthenticator); + } + + + + /******************************************************************************* + ** Setter for routeAuthenticator + *******************************************************************************/ + public void setRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + } + + + + /******************************************************************************* + ** Fluent setter for routeAuthenticator + *******************************************************************************/ + public ProcessBasedRouter withRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + return (this); + } + } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java new file mode 100644 index 00000000..5bfafbe9 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java @@ -0,0 +1,412 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.routeproviders; + + +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; +import com.kingsrook.qqq.backend.core.model.actions.processes.QProcessPayload; + + +/******************************************************************************* + ** process payload shared the processes which are used as process-based-router + ** processes. e.g., the fields here are those written to and read by + ** ProcessBasedRouter. + *******************************************************************************/ +public class ProcessBasedRouterPayload extends QProcessPayload +{ + private String path; + private String method; + private Map pathParams; + private Map queryParams; + private Map formParams; + private Map cookies; + + private Integer statusCode; + private String redirectURL; + private Map responseHeaders; + private String responseString; + private byte[] responseBytes; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ProcessBasedRouterPayload() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ProcessBasedRouterPayload(ProcessState processState) + { + this.populateFromProcessState(processState); + } + + + + /******************************************************************************* + ** Getter for path + *******************************************************************************/ + public String getPath() + { + return (this.path); + } + + + + /******************************************************************************* + ** Setter for path + *******************************************************************************/ + public void setPath(String path) + { + this.path = path; + } + + + + /******************************************************************************* + ** Fluent setter for path + *******************************************************************************/ + public ProcessBasedRouterPayload withPath(String path) + { + this.path = path; + return (this); + } + + + + /******************************************************************************* + ** Getter for method + *******************************************************************************/ + public String getMethod() + { + return (this.method); + } + + + + /******************************************************************************* + ** Setter for method + *******************************************************************************/ + public void setMethod(String method) + { + this.method = method; + } + + + + /******************************************************************************* + ** Fluent setter for method + *******************************************************************************/ + public ProcessBasedRouterPayload withMethod(String method) + { + this.method = method; + return (this); + } + + + + /******************************************************************************* + ** Getter for pathParams + *******************************************************************************/ + public Map getPathParams() + { + return (this.pathParams); + } + + + + /******************************************************************************* + ** Setter for pathParams + *******************************************************************************/ + public void setPathParams(Map pathParams) + { + this.pathParams = pathParams; + } + + + + /******************************************************************************* + ** Fluent setter for pathParams + *******************************************************************************/ + public ProcessBasedRouterPayload withPathParams(Map pathParams) + { + this.pathParams = pathParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for queryParams + *******************************************************************************/ + public Map getQueryParams() + { + return (this.queryParams); + } + + + + /******************************************************************************* + ** Setter for queryParams + *******************************************************************************/ + public void setQueryParams(Map queryParams) + { + this.queryParams = queryParams; + } + + + + /******************************************************************************* + ** Fluent setter for queryParams + *******************************************************************************/ + public ProcessBasedRouterPayload withQueryParams(Map queryParams) + { + this.queryParams = queryParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for formParams + *******************************************************************************/ + public Map getFormParams() + { + return (this.formParams); + } + + + + /******************************************************************************* + ** Setter for formParams + *******************************************************************************/ + public void setFormParams(Map formParams) + { + this.formParams = formParams; + } + + + + /******************************************************************************* + ** Fluent setter for formParams + *******************************************************************************/ + public ProcessBasedRouterPayload withFormParams(Map formParams) + { + this.formParams = formParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for cookies + *******************************************************************************/ + public Map getCookies() + { + return (this.cookies); + } + + + + /******************************************************************************* + ** Setter for cookies + *******************************************************************************/ + public void setCookies(Map cookies) + { + this.cookies = cookies; + } + + + + /******************************************************************************* + ** Fluent setter for cookies + *******************************************************************************/ + public ProcessBasedRouterPayload withCookies(Map cookies) + { + this.cookies = cookies; + return (this); + } + + + + /******************************************************************************* + ** Getter for statusCode + *******************************************************************************/ + public Integer getStatusCode() + { + return (this.statusCode); + } + + + + /******************************************************************************* + ** Setter for statusCode + *******************************************************************************/ + public void setStatusCode(Integer statusCode) + { + this.statusCode = statusCode; + } + + + + /******************************************************************************* + ** Fluent setter for statusCode + *******************************************************************************/ + public ProcessBasedRouterPayload withStatusCode(Integer statusCode) + { + this.statusCode = statusCode; + return (this); + } + + + + /******************************************************************************* + ** Getter for responseHeaders + *******************************************************************************/ + public Map getResponseHeaders() + { + return (this.responseHeaders); + } + + + + /******************************************************************************* + ** Setter for responseHeaders + *******************************************************************************/ + public void setResponseHeaders(Map responseHeaders) + { + this.responseHeaders = responseHeaders; + } + + + + /******************************************************************************* + ** Fluent setter for responseHeaders + *******************************************************************************/ + public ProcessBasedRouterPayload withResponseHeaders(Map responseHeaders) + { + this.responseHeaders = responseHeaders; + return (this); + } + + + + /******************************************************************************* + ** Getter for responseString + *******************************************************************************/ + public String getResponseString() + { + return (this.responseString); + } + + + + /******************************************************************************* + ** Setter for responseString + *******************************************************************************/ + public void setResponseString(String responseString) + { + this.responseString = responseString; + } + + + + /******************************************************************************* + ** Fluent setter for responseString + *******************************************************************************/ + public ProcessBasedRouterPayload withResponseString(String responseString) + { + this.responseString = responseString; + return (this); + } + + + + /******************************************************************************* + ** Getter for responseBytes + *******************************************************************************/ + public byte[] getResponseBytes() + { + return (this.responseBytes); + } + + + + /******************************************************************************* + ** Setter for responseBytes + *******************************************************************************/ + public void setResponseBytes(byte[] responseBytes) + { + this.responseBytes = responseBytes; + } + + + + /******************************************************************************* + ** Fluent setter for responseBytes + *******************************************************************************/ + public ProcessBasedRouterPayload withResponseBytes(byte[] responseBytes) + { + this.responseBytes = responseBytes; + return (this); + } + + + + /******************************************************************************* + ** Getter for redirectURL + *******************************************************************************/ + public String getRedirectURL() + { + return (this.redirectURL); + } + + + + /******************************************************************************* + ** Setter for redirectURL + *******************************************************************************/ + public void setRedirectURL(String redirectURL) + { + this.redirectURL = redirectURL; + } + + + + /******************************************************************************* + ** Fluent setter for redirectURL + *******************************************************************************/ + public ProcessBasedRouterPayload withRedirectURL(String redirectURL) + { + this.redirectURL = redirectURL; + return (this); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java index ac16e283..c6504be1 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java @@ -22,22 +22,40 @@ package com.kingsrook.qqq.middleware.javalin.routeproviders; +import java.net.URL; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; +import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface; import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; +import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface; +import io.javalin.Javalin; import io.javalin.config.JavalinConfig; +import io.javalin.http.Context; import io.javalin.http.staticfiles.Location; import io.javalin.http.staticfiles.StaticFileConfig; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* - ** + ** javalin route provider that hosts a path in the http server via a path on + ** the file system *******************************************************************************/ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInterface { - private final String hostedPath; - private final String fileSystemPath; - private QInstance qInstance; + private static final QLogger LOG = QLogger.getLogger(SimpleFileSystemDirectoryRouter.class); + + private final String hostedPath; + private final String fileSystemPath; + + private QCodeReference routeAuthenticator; + + private QInstance qInstance; @@ -59,6 +77,7 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt public SimpleFileSystemDirectoryRouter(JavalinRouteProviderMetaData routeProvider) { this(routeProvider.getHostedPath(), routeProvider.getFileSystemPath()); + setRouteAuthenticator(routeProvider.getRouteAuthenticator()); } @@ -74,17 +93,132 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt + /*************************************************************************** + ** + ***************************************************************************/ + private void handleJavalinStaticFileConfig(StaticFileConfig staticFileConfig) + { + URL resource = getClass().getClassLoader().getResource(fileSystemPath); + if(resource == null) + { + String message = "Could not find file system path: " + fileSystemPath; + if(fileSystemPath.startsWith("/") && getClass().getClassLoader().getResource(fileSystemPath.replaceFirst("^/+", "")) != null) + { + message += ". For non-absolute paths, do not prefix with a leading slash."; + } + throw new RuntimeException(message); + } + + if(!hostedPath.startsWith("/")) + { + LOG.warn("hostedPath [" + hostedPath + "] should probably start with a leading slash..."); + } + + staticFileConfig.directory = resource.getFile(); + staticFileConfig.hostedPath = hostedPath; + staticFileConfig.location = Location.EXTERNAL; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void before(Context context) throws QException + { + LOG.debug("In before handler for simpleFileSystemRouter", logPair("hostedPath", hostedPath)); + QContext.init(qInstance, new QSystemUserSession()); + + if(routeAuthenticator != null) + { + try + { + RouteAuthenticatorInterface routeAuthenticator = QCodeLoader.getAdHoc(RouteAuthenticatorInterface.class, this.routeAuthenticator); + boolean isAuthenticated = routeAuthenticator.authenticateRequest(context); + if(!isAuthenticated) + { + LOG.info("Static file request is not authenticated, so telling javalin to skip remaining handlers", logPair("path", context.path())); + context.skipRemainingHandlers(); + } + } + catch(Exception e) + { + context.skipRemainingHandlers(); + QJavalinImplementation.handleException(context, e); + } + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void after(Context context) + { + LOG.debug("In after handler for simpleFileSystemRouter", logPair("hostedPath", hostedPath)); + QContext.clear(); + } + + + /*************************************************************************** ** ***************************************************************************/ @Override public void acceptJavalinConfig(JavalinConfig config) { - config.staticFiles.add((StaticFileConfig userConfig) -> - { - userConfig.hostedPath = hostedPath; - userConfig.directory = fileSystemPath; - userConfig.location = Location.EXTERNAL; - }); + config.staticFiles.add(this::handleJavalinStaticFileConfig); } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void acceptJavalinService(Javalin service) + { + String javalinPath = hostedPath; + if(!javalinPath.endsWith("/")) + { + javalinPath += "/"; + } + javalinPath += ""; + + service.before(javalinPath, this::before); + service.before(javalinPath, this::after); + } + + + + /******************************************************************************* + ** Getter for routeAuthenticator + *******************************************************************************/ + public QCodeReference getRouteAuthenticator() + { + return (this.routeAuthenticator); + } + + + + /******************************************************************************* + ** Setter for routeAuthenticator + *******************************************************************************/ + public void setRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + } + + + + /******************************************************************************* + ** Fluent setter for routeAuthenticator + *******************************************************************************/ + public SimpleFileSystemDirectoryRouter withRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + return (this); + } + } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/RouteAuthenticatorInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/RouteAuthenticatorInterface.java new file mode 100644 index 00000000..c08b09ef --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/RouteAuthenticatorInterface.java @@ -0,0 +1,43 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.routeproviders.authentication; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import io.javalin.http.Context; + + +/******************************************************************************* + ** interface used by QJavalinRouteProviderInterface subclasses, to interact with + ** QQQ Authentication modules, to provide authentication to custom javalin routes. + *******************************************************************************/ +public interface RouteAuthenticatorInterface +{ + + /*************************************************************************** + ** where authentication for a route occurs, before the route is served. + ** + ** @return true if request is authenticated; else false + ***************************************************************************/ + boolean authenticateRequest(Context context) throws QException; + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java new file mode 100644 index 00000000..4cf78271 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java @@ -0,0 +1,76 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.routeproviders.authentication; + + +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import io.javalin.http.Context; + + +/******************************************************************************* + ** simple implementation of a route authenticator. Assumes that unauthenticated + ** requests should redirect to a login page. Note though, maybe that should be + ** more intelligent, like, only redirect requets for a .html file, but not + ** requests for include files like images or .js/.css? + *******************************************************************************/ +public class SimpleRouteAuthenticator implements RouteAuthenticatorInterface +{ + private static final QLogger LOG = QLogger.getLogger(SimpleRouteAuthenticator.class); + + + /*************************************************************************** + ** + ***************************************************************************/ + public boolean authenticateRequest(Context context) throws QException + { + try + { + QSession qSession = QJavalinImplementation.setupSession(context, null); + LOG.debug("Session has been activated", "uuid=" + qSession.getUuid()); + return (true); + } + catch(QAuthenticationException e) + { + QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(QContext.getQInstance().getAuthentication()); + + String redirectURL = authenticationModule.getLoginRedirectUrl(context.fullUrl()); + + context.redirect(redirectURL); + LOG.debug("Redirecting request, due to required session missing"); + return (false); + } + catch(QModuleDispatchException e) + { + throw (new QException("Error authenticating request", e)); + } + } + +} diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index 6ea236c6..d1e31bd0 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -22,10 +22,8 @@ package com.kingsrook.qqq.backend.javalin; -import java.io.File; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; import java.sql.Connection; import java.util.ArrayList; import java.util.HashMap; @@ -98,6 +96,8 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; +import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouterPayload; +import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.SimpleRouteAuthenticator; import org.apache.commons.io.IOUtils; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -126,7 +126,7 @@ public class TestUtils public static final String SCREEN_0 = "screen0"; public static final String SCREEN_1 = "screen1"; - public static final String STATIC_SITE_PATH = Paths.get("").toAbsolutePath() + "/static-site"; + public static final String STATIC_SITE_PATH = "static-site"; @@ -134,12 +134,9 @@ public class TestUtils ** Prime a test database (e.g., h2, in-memory) ** *******************************************************************************/ - @SuppressWarnings("unchecked") public static void primeTestDatabase() throws Exception { - ConnectionManager connectionManager = new ConnectionManager(); - - try(Connection connection = connectionManager.getConnection(TestUtils.defineDefaultH2Backend())) + try(Connection connection = ConnectionManager.getConnection(TestUtils.defineDefaultH2Backend())) { InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql"); assertNotNull(primeTestDatabaseSqlStream); @@ -161,8 +158,7 @@ public class TestUtils *******************************************************************************/ public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception { - ConnectionManager connectionManager = new ConnectionManager(); - try(Connection connection = connectionManager.getConnection(defineDefaultH2Backend())) + try(Connection connection = ConnectionManager.getConnection(defineDefaultH2Backend())) { QueryManager.executeStatement(connection, sql, resultSetProcessor); } @@ -195,17 +191,24 @@ public class TestUtils defineWidgets(qInstance); List routeProviders = new ArrayList<>(); - if(new File(STATIC_SITE_PATH).exists()) - { - routeProviders.add(new JavalinRouteProviderMetaData() - .withHostedPath("/statically-served") - .withFileSystemPath(STATIC_SITE_PATH)); - } + routeProviders.add(new JavalinRouteProviderMetaData() + .withHostedPath("/statically-served") + .withFileSystemPath(STATIC_SITE_PATH)); + + routeProviders.add(new JavalinRouteProviderMetaData() + .withHostedPath("/protected-statically-served") + .withFileSystemPath(STATIC_SITE_PATH) + .withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class))); routeProviders.add(new JavalinRouteProviderMetaData() .withHostedPath("/served-by-process/") .withProcessName("routerProcess")); + routeProviders.add(new JavalinRouteProviderMetaData() + .withHostedPath("/protected-served-by-process/") + .withProcessName("routerProcess") + .withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class))); + qInstance.withSupplementalMetaData(new QJavalinMetaData().withRouteProviders(routeProviders)); qInstance.addBackend(defineMemoryBackend()); @@ -237,8 +240,10 @@ public class TestUtils .withName("step") .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> { - String path = runBackendStepInput.getValueString("path"); - runBackendStepOutput.addValue("response", "So you've asked for: " + path); + ProcessBasedRouterPayload processPayload = runBackendStepInput.getProcessPayload(ProcessBasedRouterPayload.class); + String path = processPayload.getPath(); + processPayload.setResponseString("So you've asked for: " + path); + runBackendStepOutput.setProcessPayload(processPayload); })) )); } @@ -793,7 +798,7 @@ public class TestUtils { return (new RenderWidgetOutput(new RawHTML("title", QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE_OFFSET_MINUTES) - + "|" + QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE) + + "|" + QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE) ))); } } diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java index 02408873..e835e724 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java @@ -22,23 +22,18 @@ package com.kingsrook.qqq.middleware.javalin; -import java.io.File; import java.io.IOException; -import java.nio.charset.Charset; import java.util.List; -import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.javalin.TestUtils; import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +import io.javalin.http.HttpStatus; import kong.unirest.HttpResponse; import kong.unirest.Unirest; -import org.apache.commons.io.FileUtils; import org.json.JSONObject; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -55,17 +50,6 @@ class QApplicationJavalinServerTest - /******************************************************************************* - ** - *******************************************************************************/ - @BeforeEach - void beforeEach() throws IOException - { - FileUtils.writeStringToFile(new File(TestUtils.STATIC_SITE_PATH + "/foo.html"), "Foo? Bar!", Charset.defaultCharset()); - } - - - /******************************************************************************* ** *******************************************************************************/ @@ -74,8 +58,6 @@ class QApplicationJavalinServerTest { javalinServer.stop(); TestApplication.callCount = 0; - - FileUtils.deleteDirectory(new File(TestUtils.STATIC_SITE_PATH)); } @@ -212,6 +194,35 @@ class QApplicationJavalinServerTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAuthenticatedStaticRouter() throws Exception + { + javalinServer = new QApplicationJavalinServer(getQqqApplication()) + .withServeFrontendMaterialDashboard(false) + .withPort(PORT); + javalinServer.start(); + + Unirest.config().setDefaultResponseEncoding("UTF-8") + .followRedirects(false); + + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/protected-statically-served/foo.html") + .header("Authorization", "Bearer Deny") + .asString(); + + assertEquals(HttpStatus.FOUND.getCode(), response.getStatus()); + assertThat(response.getHeaders().getFirst("Location")).contains("createMockSession"); + + response = Unirest.get("http://localhost:" + PORT + "/protected-statically-served/foo.html") + .asString(); + assertEquals(HttpStatus.OK.getCode(), response.getStatus()); + assertEquals("Foo? Bar!", response.getBody()); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -230,6 +241,35 @@ class QApplicationJavalinServerTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAuthenticatedProcessRouter() throws Exception + { + javalinServer = new QApplicationJavalinServer(getQqqApplication()) + .withServeFrontendMaterialDashboard(false) + .withPort(PORT); + javalinServer.start(); + + Unirest.config().setDefaultResponseEncoding("UTF-8") + .followRedirects(false); + + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/protected-served-by-process/foo.html") + .header("Authorization", "Bearer Deny") + .asString(); + + assertEquals(HttpStatus.FOUND.getCode(), response.getStatus()); + assertThat(response.getHeaders().getFirst("Location")).contains("createMockSession"); + + response = Unirest.get("http://localhost:" + PORT + "/protected-statically-served/foo.html") + .asString(); + assertEquals(200, response.getStatus()); + assertEquals("So you've asked for: /served-by-process/foo.html", response.getBody()); + } + + + /*************************************************************************** ** ***************************************************************************/ diff --git a/qqq-middleware-javalin/src/test/resources/static-site/foo.html b/qqq-middleware-javalin/src/test/resources/static-site/foo.html new file mode 100644 index 00000000..22b50adf --- /dev/null +++ b/qqq-middleware-javalin/src/test/resources/static-site/foo.html @@ -0,0 +1 @@ +Foo? Bar! \ No newline at end of file