Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion openidm-servlet/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
~ your own identifying information:
~ "Portions Copyrighted [year] [name of copyright owner]"
~
~ Portions Copyrighted 2024-2025 3A Systems LLC.
~ Portions Copyrighted 2024-2026 3A Systems LLC.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
Expand Down Expand Up @@ -80,6 +80,21 @@
<artifactId>org.apache.felix.framework</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
*/
package org.forgerock.openidm.ui.internal.service;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Map;
Expand Down Expand Up @@ -122,10 +122,11 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
logger.debug("GET call on {}", req);

// the request pathInfo is always null for root contexts
// path info is null when the request names the context root itself without a trailing slash
String target = req.getPathInfo();
if (target == null || "".equals(target)) {
res.sendRedirect(req.getServletPath() + "/");
if (target == null || target.isEmpty()) {
// redirect to the configured context root rather than anything taken from the request
res.sendRedirect("/".equals(contextRoot) ? "/" : contextRoot + "/");
} else {
if ("/".equals(target)) {
target = "/index.html";
Expand All @@ -144,37 +145,62 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res)
}

// Locate the file in extension dir first, fall back to default dir
URL url = null;
String loadDir = (String) PropertyUtil.substVars(extensionDir, IdentityServer.getInstance(), false);
File file = new File(loadDir + target);
if (file.getCanonicalPath().startsWith(new File(loadDir).getCanonicalPath())
&& file.exists() && !file.isDirectory()) {
url = file.getCanonicalFile().toURI().toURL();
} else {
loadDir = (String) PropertyUtil.substVars(defaultDir, IdentityServer.getInstance(), false);
file = new File(loadDir + target);
if (file.getCanonicalPath().startsWith(new File(loadDir).getCanonicalPath())
&& file.exists() && !file.isDirectory()) {
url = file.getCanonicalFile().toURI().toURL();
}
Path file = locate(extensionDir, target);
if (file == null) {
file = locate(defaultDir, target);
}

if (url == null) {
if (file == null) {
res.sendError(HttpServletResponse.SC_NOT_FOUND);
} else if (target.equals("/index.html")) {
handleIndexHtml(res, url);
handleIndexHtml(res, file);
} else {
handle(req, res, url, target);
handle(req, res, file, target);
}
}
}

/**
* Resolves a request path against one of the configured resource directories.
* <p>
* Containment is checked on path components ({@link Path#startsWith(Path)}), never on a
* string prefix, so a sibling directory such as {@code default-old} is not reachable from
* {@code default}. Symbolic links are resolved on both sides before the final check.
*
* @param dir the configured directory, possibly containing {@code &{...}} property references
* @param target the request path, always starting with {@code /}
* @return the real path of the regular file denoted by {@code target} inside {@code dir},
* or {@code null} if the directory does not exist, the file does not exist, is not a
* regular file, or lies outside the directory
*/
private Path locate(String dir, String target) {
String loadDir = (String) PropertyUtil.substVars(dir, IdentityServer.getInstance(), false);
Path base;
Path file;
try {
base = Paths.get(loadDir).toAbsolutePath().normalize();
file = base.resolve(target.substring(1)).normalize();
} catch (InvalidPathException e) {
return null;
}
if (!file.startsWith(base) || !Files.isDirectory(base) || !Files.isRegularFile(file)) {
return null;
}
try {
Path realFile = file.toRealPath();
return realFile.startsWith(base.toRealPath()) ? realFile : null;
} catch (IOException e) {
// vanished or unreadable between the check above and here
return null;
}
}

/**
* Serves index.html with the openidm.context.path injected as a global JS variable.
* Replaces {@code </head>} with a small inline script that sets
* {@code window.__openidm_context_path} before RequireJS boots.
*/
private void handleIndexHtml(HttpServletResponse res, URL url) throws IOException {
private void handleIndexHtml(HttpServletResponse res, Path file) throws IOException {
res.setContentType("text/html");
res.setHeader("Cache-Control", "no-cache");

Expand All @@ -185,11 +211,7 @@ private void handleIndexHtml(HttpServletResponse res, URL url) throws IOExceptio
// Strip leading slash — the UI Constants.context value does not include it
String contextValue = contextPath.substring(1);

byte[] raw;
try (InputStream is = url.openStream()) {
raw = is.readAllBytes();
}
String html = new String(raw, StandardCharsets.UTF_8);
String html = new String(Files.readAllBytes(file), StandardCharsets.UTF_8);

// Inject a tiny script right before </head> so it is available before RequireJS loads.
// Escape characters that could break out of the JS string or the script tag.
Expand Down Expand Up @@ -259,7 +281,7 @@ private void clear() {
logger.debug("Unregistered UI servlet at {}", contextRoot);
}

private void handle(HttpServletRequest req, HttpServletResponse res, URL url, String resName)
private void handle(HttpServletRequest req, HttpServletResponse res, Path file, String resName)
throws IOException {
String contentType = getServletContext().getMimeType(resName);
if (contentType != null) {
Expand All @@ -268,39 +290,24 @@ private void handle(HttpServletRequest req, HttpServletResponse res, URL url, St
res.setContentType(getMimeType(resName));
}

long lastModified = getLastModified(url);
long lastModified = getLastModified(file);
if (lastModified != 0) {
res.setDateHeader("Last-Modified", lastModified);
}

if (!resourceModified(lastModified, req.getDateHeader("If-Modified-Since"))) {
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
} else {
copyResource(url, res);
copyResource(file, res);
}
}

private long getLastModified(URL url) {
long lastModified = 0;

private long getLastModified(Path file) {
try {
URLConnection conn = url.openConnection();
lastModified = conn.getLastModified();
} catch (Exception e) {
// Do nothing
return Files.getLastModifiedTime(file).toMillis();
} catch (IOException e) {
return 0;
}

if (lastModified == 0) {
String filepath = url.getPath();
if (filepath != null) {
File f = new File(filepath);
if (f.exists()) {
lastModified = f.lastModified();
}
}
}

return lastModified;
}

private String getMimeType(String fileName) {
Expand All @@ -324,33 +331,11 @@ private boolean resourceModified(long resTimestamp, long modSince) {
return resTimestamp == 0 || modSince == -1 || resTimestamp > modSince;
}

private void copyResource(URL url, HttpServletResponse res)
private void copyResource(Path file, HttpServletResponse res)
throws IOException {
OutputStream os = null;
InputStream is = null;

try {
os = res.getOutputStream();
is = url.openStream();

int len = 0;
byte[] buf = new byte[1024];
int n;

while ((n = is.read(buf, 0, buf.length)) >= 0) {
os.write(buf, 0, n);
len += n;
}

res.setContentLength(len);
} finally {
if (is != null) {
is.close();
}

if (os != null) {
os.close();
}
res.setContentLengthLong(Files.size(file));
try (OutputStream os = res.getOutputStream()) {
Files.copy(file, os);
}
}

Expand Down
Loading
Loading