Skip to content

Commit

Permalink
O3-310: allow Put operations on an application-writable config.json file
Browse files Browse the repository at this point in the history
config.json file name shouldn't be dynamic

dynamically serve 'frontend/config.json' endpoint

remove unrequited change
  • Loading branch information
jnsereko committed Jun 11, 2023
1 parent 589f30b commit 6946485
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 11 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/ |

Original file line number Diff line number Diff line change
Expand Up @@ -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";

}

126 changes: 120 additions & 6 deletions omod/src/main/java/org/openmrs/module/spa/servlet/SpaServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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
*
Expand Down Expand Up @@ -62,6 +74,84 @@ 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")) {
if (!Context.isAuthenticated()) {
String basicAuth = request.getHeader("Authorization");
if (basicAuth != null) {
// check that header is in format "Basic ${base64encode(username + ":" + password)}"
if (isValidAuthFormat(response, basicAuth)) return;
}
}

User user = Context.getAuthenticatedUser();
if (user != null && user.isSuperUser()) {
saveJsonConfigFile(request, response);
} else {
log.error("Authorisation error while creating a config.json file");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
}

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);

Expand All @@ -70,13 +160,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());
}
}
Expand All @@ -89,12 +182,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());
Expand All @@ -117,6 +213,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
Expand Down Expand Up @@ -154,8 +254,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
Expand All @@ -173,9 +279,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);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* 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.DispatcherType;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import java.util.EnumSet;

import org.openmrs.module.spa.filter.SpaFilter;
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 openmrsServletReg = servletContext.getServletRegistration("openmrs");
openmrsServletReg.addMapping("/frontend/*");

ServletRegistration servletReg = servletContext.addServlet("spaServlet", new SpaServlet());
servletReg.addMapping("/frontend/config.json");

Dynamic filter = servletContext.addFilter("spaFilter", new SpaFilter());
filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/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();
}
}
}

0 comments on commit 6946485

Please sign in to comment.