-
Notifications
You must be signed in to change notification settings - Fork 48
O3-310: allow Put operations on an application-writable config.json file to openmrs/data/frontends/config.json #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,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(); | ||
jnsereko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 +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()); | ||
| } | ||
| } | ||
|
|
@@ -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()); | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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,50 @@ | ||
| /** | ||
| * 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.directwebremoting.servlet.EfficientShutdownServletContextAttributeListener; | ||
| import org.openmrs.module.spa.filter.SpaFilter; | ||
| import org.openmrs.module.spa.servlet.SpaServlet; | ||
| import org.openmrs.web.SessionListener; | ||
| 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("/ws/frontend/*"); | ||
|
|
||
| ServletRegistration servletReg = servletContext.addServlet("spaServlet", new SpaServlet()); | ||
| servletReg.addMapping("/ws/frontend/config.json"); | ||
|
|
||
| Dynamic filter = servletContext.addFilter("spaFilter", new SpaFilter()); | ||
|
||
| filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/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(); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.