Refactor loading of theme resources (#33326)

Closes #33325

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-10-01 08:02:05 +02:00 committed by GitHub
parent 8769fed585
commit 4a2fbf5339
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 175 additions and 114 deletions

View file

@ -17,33 +17,18 @@
package org.keycloak.quarkus.runtime.themes; package org.keycloak.quarkus.runtime.themes;
import org.keycloak.theme.ClasspathThemeResourceProviderFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Locale; import java.util.Locale;
import java.util.Properties; import java.util.Properties;
import org.keycloak.theme.ClasspathThemeResourceProviderFactory;
public class FlatClasspathThemeResourceProviderFactory extends ClasspathThemeResourceProviderFactory { public class FlatClasspathThemeResourceProviderFactory extends ClasspathThemeResourceProviderFactory {
public static final String ID = "flat-classpath"; public static final String ID = "flat-classpath";
@Override
public InputStream getResourceAsStream(String path) throws IOException {
Enumeration<URL> resources = classLoader.getResources(THEME_RESOURCES_RESOURCES);
while (resources.hasMoreElements()) {
InputStream is = getResourceAsStream(path, resources.nextElement());
if (is != null) {
return is;
}
}
return null;
}
@Override @Override
public Properties getMessages(String baseBundlename, Locale locale) throws IOException { public Properties getMessages(String baseBundlename, Locale locale) throws IOException {
Properties messages = new Properties(); Properties messages = new Properties();

View file

@ -1,78 +1,38 @@
package org.keycloak.encoding; package org.keycloak.encoding;
import java.io.IOException;
import java.nio.file.Files;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession; import org.keycloak.theme.ResourceLoader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files;
import java.util.zip.GZIPOutputStream; import java.util.zip.GZIPOutputStream;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
public class GzipResourceEncodingProvider implements ResourceEncodingProvider { public class GzipResourceEncodingProvider implements ResourceEncodingProvider {
private static final Logger logger = Logger.getLogger(ResourceEncodingProvider.class); private static final Logger logger = Logger.getLogger(ResourceEncodingProvider.class);
private KeycloakSession session; private final File cacheDir;
private File cacheDir;
public GzipResourceEncodingProvider(KeycloakSession session, File cacheDir) { public GzipResourceEncodingProvider(File cacheDir) {
this.session = session;
this.cacheDir = cacheDir; this.cacheDir = cacheDir;
} }
public InputStream getEncodedStream(StreamSupplier producer, String... path) { public InputStream getEncodedStream(StreamSupplier producer, String... path) {
StringBuilder sb = new StringBuilder();
sb.append(cacheDir.getAbsolutePath());
for (String p : path) {
sb.append(File.separatorChar);
sb.append(p);
}
sb.append(".gz");
String filePath = sb.toString();
try { try {
File encodedFile = new File(filePath); File encodedFile = ResourceLoader.getFile(cacheDir, String.join("/", path) + ".gz");
if (!encodedFile.getCanonicalPath().startsWith(cacheDir.getCanonicalPath())) { if (encodedFile == null) {
return null; return null;
} }
if (!encodedFile.exists()) { if (!encodedFile.exists()) {
InputStream is = producer.getInputStream(); encodedFile = createEncodedFile(producer, encodedFile);
if (is != null) {
File parent = encodedFile.getParentFile();
if (!parent.isDirectory()) {
parent.mkdirs();
}
File tmpEncodedFile = File.createTempFile(
encodedFile.getName(),
"tmp",
parent);
FileOutputStream fos = new FileOutputStream(tmpEncodedFile);
GZIPOutputStream gos = new GZIPOutputStream(fos);
IOUtils.copy(is, gos);
gos.close();
is.close();
try {
Files.move(
tmpEncodedFile.toPath(),
encodedFile.toPath(),
REPLACE_EXISTING);
} catch ( IOException io ) {
logger.warnf("Fail to move %s %s", tmpEncodedFile.toString(), io);
if (!encodedFile.exists()) {
encodedFile = null;
}
}
} else {
encodedFile = null;
}
} }
return encodedFile != null ? new FileInputStream(encodedFile) : null; return encodedFile != null ? new FileInputStream(encodedFile) : null;
@ -86,4 +46,31 @@ public class GzipResourceEncodingProvider implements ResourceEncodingProvider {
return "gzip"; return "gzip";
} }
private File createEncodedFile(StreamSupplier producer, File target) throws IOException {
InputStream is = producer.getInputStream();
if (is == null) {
return null;
}
File parent = target.getParentFile();
if (!parent.isDirectory()) {
if (parent.mkdirs() && !parent.isDirectory()) {
logger.warnf("Fail to create cache directory %s", parent.toString());
}
}
File tmpEncodedFile = File.createTempFile(target.getName(), "tmp", parent);
try (is; GZIPOutputStream gos = new GZIPOutputStream(new FileOutputStream(tmpEncodedFile))) {
IOUtils.copy(is, gos);
}
try {
Files.move(tmpEncodedFile.toPath(), target.toPath(), REPLACE_EXISTING);
return target;
} catch (IOException io) {
logger.warnf(io, "Fail to move temporary file to %s", target.toString());
return null;
}
}
} }

View file

@ -11,6 +11,7 @@ import org.keycloak.provider.ProviderConfigurationBuilder;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -29,15 +30,13 @@ public class GzipResourceEncodingProviderFactory implements ResourceEncodingProv
cacheDir = initCacheDir(); cacheDir = initCacheDir();
} }
return new GzipResourceEncodingProvider(session, cacheDir); return new GzipResourceEncodingProvider(cacheDir);
} }
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
String e = config.get("excludedContentTypes", "image/png image/jpeg"); String e = config.get("excludedContentTypes", "image/png image/jpeg");
for (String s : e.split(" ")) { excludedContentTypes.addAll(Arrays.asList(e.split(" ")));
excludedContentTypes.add(s);
}
} }
@Override @Override

View file

@ -107,24 +107,7 @@ public class ClassLoaderTheme implements Theme {
@Override @Override
public InputStream getResourceAsStream(String path) throws IOException { public InputStream getResourceAsStream(String path) throws IOException {
final URL rootResourceURL = classLoader.getResource(resourceRoot); return ResourceLoader.getResourceAsStream(resourceRoot, path);
if (rootResourceURL == null) {
return null;
}
String rootPath = rootResourceURL.getPath();
if (rootPath.endsWith("//")) {
// needed for asset loading in quarkus IDELauncher - see gh issue #9942
rootPath = rootPath.substring(0, rootPath.length() -1);
}
final URL resourceURL = classLoader.getResource(resourceRoot + path);
if(resourceURL == null || !resourceURL.getPath().startsWith(rootPath)) {
return null;
}
else {
return resourceURL.openConnection().getInputStream();
}
} }
@Override @Override

View file

@ -41,21 +41,7 @@ public class ClasspathThemeResourceProviderFactory implements ThemeResourceProvi
@Override @Override
public InputStream getResourceAsStream(String path) throws IOException { public InputStream getResourceAsStream(String path) throws IOException {
return getResourceAsStream(path, classLoader.getResource(THEME_RESOURCES_RESOURCES)); return ResourceLoader.getResourceAsStream(THEME_RESOURCES_RESOURCES, path);
}
protected InputStream getResourceAsStream(String path, URL rootResourceURL) throws IOException {
if (rootResourceURL == null) {
return null;
}
final String rootPath = rootResourceURL.getPath();
final URL resourceURL = classLoader.getResource(THEME_RESOURCES_RESOURCES + path);
if(resourceURL == null || !resourceURL.getPath().startsWith(rootPath)) {
return null;
}
else {
return resourceURL.openConnection().getInputStream();
}
} }
@Override @Override

View file

@ -90,16 +90,7 @@ public class FolderTheme implements Theme {
@Override @Override
public InputStream getResourceAsStream(String path) throws IOException { public InputStream getResourceAsStream(String path) throws IOException {
if (File.separatorChar != '/') { return ResourceLoader.getFileAsStream(resourcesDir, path);
path = path.replace('/', File.separatorChar);
}
File file = new File(resourcesDir, path);
if (!file.isFile() || !file.getCanonicalPath().startsWith(resourcesDir.getCanonicalPath() + File.separator)) {
return null;
} else {
return file.toURI().toURL().openStream();
}
} }
@Override @Override

View file

@ -0,0 +1,47 @@
package org.keycloak.theme;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Path;
public class ResourceLoader {
public static InputStream getResourceAsStream(String root, String resource) throws IOException {
if (root == null || resource == null) {
return null;
}
Path rootPath = Path.of("/", root).normalize().toAbsolutePath();
Path resourcePath = rootPath.resolve(resource).normalize().toAbsolutePath();
if (resourcePath.startsWith(rootPath)) {
URL url = classLoader().getResource(resourcePath.toString().substring(1));
return url != null ? url.openStream() : null;
} else {
return null;
}
}
public static InputStream getFileAsStream(File root, String resource) throws IOException {
File file = getFile(root, resource);
return file != null && file.isFile() ? file.toURI().toURL().openStream() : null;
}
public static File getFile(File root, String resource) throws IOException {
if (root == null || resource == null) {
return null;
}
Path rootPath = root.toPath().normalize().toAbsolutePath();
Path resourcePath = rootPath.resolve(resource).normalize().toAbsolutePath();
if (resourcePath.startsWith(rootPath)) {
return resourcePath.toFile();
} else {
return null;
}
}
private static ClassLoader classLoader() {
return Thread.currentThread().getContextClassLoader();
}
}

View file

@ -0,0 +1,79 @@
package org.keycloak.theme;
import org.junit.Assert;
import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class ResourceLoaderTest {
static String NONE = "../";
static String SINGLE = "%2E%2E%2F";
static String DOUBLE = "%252E%252E%252F";
@Test
public void testResource() throws IOException {
String parent = "dummy-resources/parent";
assertResourceAsStream(parent, "myresource.css", true, true);
assertResourceAsStream(parent, NONE + "myresource.css", false, true);
assertResourceAsStream(parent, SINGLE + "myresource.css", false, false);
assertResourceAsStream(parent, DOUBLE + "myresource.css", false, false);
assertResourceAsStream(parent, "one/" + NONE + "myresource.css", true, true);
assertResourceAsStream(parent, "one/" + SINGLE + "myresource.css", false, false);
assertResourceAsStream(parent, "one/" + DOUBLE + "myresource.css", false, false);
assertResourceAsStream(parent, "one/two/" + NONE + NONE + "myresource.css", true, true);
assertResourceAsStream(parent, "one/" + NONE + NONE + "myresource.css", false, true);
}
@Test
public void testFiles() throws IOException {
Path tempDirectory = Files.createTempDirectory("safepath-test");
File parent = new File(tempDirectory.toFile(), "resources");
Assert.assertTrue(parent.mkdir());
new FileOutputStream(new File(tempDirectory.toFile(), "myresource.css")).close();
new FileOutputStream(new File(parent, "myresource.css")).close();
assertFileAsStream(parent, "myresource.css", true, true);
assertFileAsStream(parent, NONE + "myresource.css", false, true);
assertFileAsStream(parent, SINGLE + "myresource.css", false, false);
assertFileAsStream(parent, DOUBLE + "myresource.css", false, false);
assertFileAsStream(new File(tempDirectory.toFile(), "test/../resources/"), "myresource.css", true, true);
}
private void assertResourceAsStream(String parent, String resource, boolean expectValid, boolean expectResourceToExist) throws IOException {
InputStream verified = ResourceLoader.getResourceAsStream(parent, resource);
if (expectValid) {
Assert.assertNotNull(verified);
} else {
Assert.assertNull(verified);
}
if (expectResourceToExist) {
Assert.assertNotNull(ResourceLoader.class.getClassLoader().getResource(parent + "/" + resource));
}
}
private void assertFileAsStream(File parent, String resource, boolean expectValid, boolean expectFileToExist) throws IOException {
InputStream verified = ResourceLoader.getFileAsStream(parent, resource);
if (expectValid) {
Assert.assertNotNull(verified);
} else {
Assert.assertNull(verified);
}
if (expectFileToExist) {
Assert.assertTrue(new File(parent, resource).getCanonicalFile().isFile());
}
}
}

View file

@ -0,0 +1,2 @@
.invalid {
}

View file

@ -0,0 +1,2 @@
.dummy {
}