diff --git a/README.md b/README.md index ed887be..d0c4fc4 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ cd openmrs-module-spa && mvn clean install ``` ## Configurations -| Property | Description | Default Value | -| ----------- | ----------- | ------------ | -| `spa.local.directory` | The directory containing the Frontend 3.0 application's `index.html`. Can be an absolute path, or relative to the application data directory. Only used if `spa.remote.enabled` is false. | frontend | -| `spa.remote.enabled` | If enabled, serves from `spa.remote.url` instead of `spa.local.directory` | false | -| `spa.remote.url` | The URL of the Frontend 3.0 application files. Only used if `spa.remote.enabled` is true. | https://spa-modules.nyc3.digitaloceanspaces.com/@openmrs/esm-app-shell/latest/ | +| Property | Description | Default Value | +|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| `spa.local.directory` | The directory containing the Frontend 3.0 application's `index.html`. Can be an absolute path, or relative to the application data directory. Only used if `spa.remote.enabled` is false. | frontend | +| `spa.remote.enabled` | If enabled, serves from `spa.remote.url` instead of `spa.local.directory` | false | +| `spa.remote.url` | The URL of the Frontend 3.0 application files. Only used if `spa.remote.enabled` is true. | https://spa-modules.nyc3.digitaloceanspaces.com/@openmrs/esm-app-shell/latest/ | + diff --git a/omod/src/main/java/org/openmrs/module/spa/SpaConstants.java b/omod/src/main/java/org/openmrs/module/spa/SpaConstants.java index 7d73bac..a254504 100644 --- a/omod/src/main/java/org/openmrs/module/spa/SpaConstants.java +++ b/omod/src/main/java/org/openmrs/module/spa/SpaConstants.java @@ -28,5 +28,6 @@ private SpaConstants() {} public static final String GP_IS_REMOTE_ENABLED = "spa.remote.enabled"; public static final String GP_REMOTE_URL = "spa.remote.url"; + } diff --git a/omod/src/main/java/org/openmrs/module/spa/servlet/SpaServlet.java b/omod/src/main/java/org/openmrs/module/spa/servlet/SpaServlet.java index b8e5b1d..1e733d8 100644 --- a/omod/src/main/java/org/openmrs/module/spa/servlet/SpaServlet.java +++ b/omod/src/main/java/org/openmrs/module/spa/servlet/SpaServlet.java @@ -11,7 +11,12 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.map.ObjectMapper; +import org.openmrs.User; import org.openmrs.api.context.Context; +import org.openmrs.module.ModuleException; import org.openmrs.module.spa.component.ResourceLoaderComponent; import org.openmrs.module.spa.utils.SpaModuleUtils; import org.openmrs.util.OpenmrsUtil; @@ -21,10 +26,15 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Base64; @Slf4j public class SpaServlet extends HttpServlet { @@ -33,6 +43,8 @@ public class SpaServlet extends HttpServlet { private static final String BASE_URL = "/spa/spaServlet"; + private static final String JSON_CONFIG_FILE_NAME = "config.json"; + /** * Used for caching purposes * @@ -62,6 +74,81 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t } } + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + String requestURI = request.getRequestURI(); + if (!requestURI.endsWith("/config.json")) { + return; + } + String basicAuth = !Context.isAuthenticated() ? request.getHeader("Authorization") : null; + if (basicAuth != null && isValidAuthFormat(response, basicAuth)) { + return; + } + User user = Context.getAuthenticatedUser(); + if (user == null || !user.isSuperUser()) { + log.error("Authorization error while creating a config.json file"); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + return; + } + saveJsonConfigFile(request, response); + } + + private void saveJsonConfigFile(HttpServletRequest request, HttpServletResponse response) throws IOException { + File jsonConfigFile = getJsonConfigFile(); + try { + BufferedReader reader = request.getReader(); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + String requestBody = stringBuilder.toString(); + + new ObjectMapper().readTree(requestBody); // verify that is in a valid JSON format + + InputStream inputStream = new ByteArrayInputStream(requestBody.getBytes(StandardCharsets.UTF_8)); + OutputStream outStream = Files.newOutputStream(jsonConfigFile.toPath()); + OpenmrsUtil.copyFile(inputStream, outStream); + + if (jsonConfigFile.exists()) { + log.debug("file: '{}' written successfully", jsonConfigFile.getAbsolutePath()); + response.setStatus(HttpServletResponse.SC_OK); + } + } catch (JsonProcessingException e) { + log.error("Invalid JSON format", e); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + } + + private boolean isValidAuthFormat(HttpServletResponse response, String basicAuth) { + if (basicAuth.startsWith("Basic")) { + try { + // remove the leading "Basic " + basicAuth = basicAuth.substring(6); + if (StringUtils.isBlank(basicAuth)) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid credentials provided"); + return true; + } + + String decoded = new String(Base64.getDecoder().decode(basicAuth), StandardCharsets.UTF_8); + if (StringUtils.isBlank(decoded) || !decoded.contains(":")) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid credentials provided"); + return true; + } + + String[] userAndPass = decoded.split(":"); + Context.authenticate(userAndPass[0], userAndPass[1]); + log.debug("authenticated [{}]", userAndPass[0]); + } + catch (Exception ex) { + // This filter never stops execution. If the user failed to + // authenticate, that will be caught later. + log.debug("authentication exception ", ex); + } + } + return false; + } + protected void handleLocalAssets(HttpServletRequest request, HttpServletResponse response) throws IOException { File file = getFile(request); @@ -70,13 +157,16 @@ protected void handleLocalAssets(HttpServletRequest request, HttpServletResponse return; } + if (file.getAbsolutePath().endsWith("/config.json")) { + response.setContentType("application/json;charset=UTF-8"); + } response.setDateHeader("Last-Modified", file.lastModified()); addCacheControlHeader(request, response); response.setContentLength((int) file.length()); String mimeType = getServletContext().getMimeType(file.getName()); response.setContentType(mimeType); - try (InputStream is = new FileInputStream(file)) { + try (InputStream is = Files.newInputStream(file.toPath())) { OpenmrsUtil.copyFile(is, response.getOutputStream()); } } @@ -89,12 +179,15 @@ protected void handleLocalAssets(HttpServletRequest request, HttpServletResponse * @param response {@link HttpServletResponse} * @throws IOException {@link IOException} F */ - protected void handleRemoteAssets(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + protected void handleRemoteAssets(HttpServletRequest request, HttpServletResponse response) throws IOException { Resource resource = getResource(request); if (!resource.exists()) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } + if (resource.getFilename().endsWith("/config.json")) { + response.setContentType("application/json;charset=UTF-8"); + } response.setDateHeader("Last-Modified", resource.lastModified()); addCacheControlHeader(request, response); response.setContentLength((int) resource.contentLength()); @@ -117,6 +210,10 @@ protected void handleRemoteAssets(HttpServletRequest request, HttpServletRespons */ protected Resource getResource(HttpServletRequest request) { String path = request.getPathInfo(); + if (path == null) { //dynamically defined servlets see https://wiki.openmrs.org/display/docs/Module+Servlets + String url = String.valueOf(request.getRequestURL()); + path = url.substring(url.lastIndexOf('/') + 1); + } /* * we want to extract everything after /spa/spaServlet from the path info. * This should cater for sub-directories @@ -154,8 +251,14 @@ protected File getFile(HttpServletRequest request) { // all url will have a base of /spa/spaResources/ String path = request.getPathInfo(); - // we want to extract everything after /spa/spaResources/ from the path info. This should cater for sub-directories - String extractedFile = path.substring(path.indexOf('/', BASE_URL.length() - 1) + 1); + String extractedFile = ""; + if (path == null) { //dynamically defined servlets see https://wiki.openmrs.org/display/docs/Module+Servlets + String url = String.valueOf(request.getRequestURL()); + extractedFile = url.substring(url.lastIndexOf('/') + 1); + } else { + // we want to extract everything after /spa/spaResources/ from the path info. This should cater for sub-directories + extractedFile = path.substring(path.indexOf('/', BASE_URL.length() - 1) + 1); + } File folder = SpaModuleUtils.getSpaStaticFilesDir(); //Resolve default index.html @@ -173,9 +276,17 @@ protected File getFile(HttpServletRequest request) { private void addCacheControlHeader(HttpServletRequest request, HttpServletResponse response) { String path = request.getPathInfo(); - if (path.endsWith("importmap.json") || path.endsWith("import-map.json")) { + if (path != null && (path.endsWith("importmap.json") || path.endsWith("import-map.json"))) { response.setHeader("Cache-Control", "public, must-revalidate, max-age=0;"); } } + private File getJsonConfigFile() { + File folder = SpaModuleUtils.getSpaStaticFilesDir(); + if (!folder.isDirectory()) { + throw new ModuleException("SPA frontend repository is not a directory at: " + folder.getAbsolutePath()); + } + return new File(folder.getAbsolutePath(), JSON_CONFIG_FILE_NAME); + } + } diff --git a/omod/src/main/java/org/openmrs/module/spa/web/SpaWebComponentRegistrar.java b/omod/src/main/java/org/openmrs/module/spa/web/SpaWebComponentRegistrar.java new file mode 100644 index 0000000..65544d4 --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/spa/web/SpaWebComponentRegistrar.java @@ -0,0 +1,38 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.spa.web; + +import javax.servlet.ServletContext; +import javax.servlet.ServletRegistration; + +import org.openmrs.module.spa.servlet.SpaServlet; +import org.springframework.stereotype.Component; +import org.springframework.web.context.ServletContextAware; + +@Component +public class SpaWebComponentRegistrar implements ServletContextAware { + + @Override + public void setServletContext(ServletContext servletContext) { + + try { + ServletRegistration servletReg = servletContext.addServlet("spaServlet", new SpaServlet()); + servletReg.addMapping("/ws/frontend/config.json"); + } + catch (Exception ex) { + //TODO need a work around for: java.lang.IllegalStateException: Started + //Unable to configure mapping for servlet because this servlet context has already been initialized. + //This happens on running openmrs after InitializationFilter or UpdateFilter + //hence requiring a restart to see any page other than index.htm + //After a restart, all mappings will then happen within Listener.contextInitialized() + ex.printStackTrace(); + } + } +} diff --git a/openmrs.log b/openmrs.log new file mode 100644 index 0000000..e69de29