Skip to content
Merged
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
12 changes: 12 additions & 0 deletions src/main/java/de/igslandstuhl/database/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import de.igslandstuhl.database.api.Subject;
import de.igslandstuhl.database.api.Teacher;
import de.igslandstuhl.database.api.User;
import de.igslandstuhl.database.server.resources.ResourceManager;
import de.igslandstuhl.database.server.sql.SQLHelper;
import de.igslandstuhl.database.server.sql.SQLiteConnection;

Expand Down Expand Up @@ -72,6 +73,17 @@ public SQLiteConnection getConnection() {
public WebServer getWebServer() {
return webServer;
}
/**
* The resource manager for this server
*/
private final ResourceManager resourceManager = new ResourceManager();
/**
* Returns the resource manager used by this server
* @return the resource manager
*/
public ResourceManager getResourceManager() {
return resourceManager;
}

/**
* Private constructor to initialize the server instance.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package de.igslandstuhl.database.server.resources;

import java.io.File;
import java.nio.file.Path;
import java.util.regex.Matcher;

/**
* Represents a resource location with context, namespace, and resource name.
* This class is used to identify resources in the application.
Expand Down Expand Up @@ -30,4 +34,27 @@ public static ResourceLocation get(String context, String resourceID) {
public boolean isVirtual() {
return context.equals("virtual");
}
public static ResourceLocation fromPath(Path path) {
Path relativePath;
try {
relativePath = Path.of(".").relativize(path);
} catch (IllegalArgumentException e) {
relativePath = path;
}
String rel = relativePath.toString();
while (rel.startsWith(".") || rel.startsWith(File.separator)) {
rel = rel.substring(1);
}
return fromRelativePath(rel);
}
public static ResourceLocation fromPath(String path) {
return fromPath(Path.of(path));
}
public static ResourceLocation fromRelativePath(String relativePath) {
String[] parts = relativePath.split(Matcher.quoteReplacement(File.separator));
if (parts.length != 3) {
return null;
}
return new ResourceLocation(parts[0], parts[1], parts[2]);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package de.igslandstuhl.database.server.resources;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand All @@ -30,30 +29,18 @@
import de.igslandstuhl.database.server.Server;

/**
* Helper class for managing resources in the application.
* Manages Resources in the application
*/
public class ResourceHelper {

public class ResourceManager {
/**
* Checks if a zip entry name is safe (no path traversal, not absolute).
* Checks if a zip entry name is safe (to prevent zip slipping).
*/
private static boolean isSafeZipEntryName(String entryName) {
// Reject absolute paths
Path path = Paths.get(entryName).normalize();
if (path.isAbsolute()) {
return false;
}
// Reject entries containing ".." as a path segment
for (Path part : path) {
if (part.toString().equals("..")) {
return false;
}
}
// Reject entries starting with "/" or "\"
if (entryName.startsWith("/") || entryName.startsWith("\\")) {
return false;
}
return true;
private boolean isSafeZipEntryName(String entryName, Path rootDir) {
// Resolve entry against a fixed root and normalize
Path resolvedPath = rootDir.resolve(entryName).normalize();

// Entry is safe if it stays within the root directory
return resolvedPath.startsWith(rootDir);
}

/**
Expand All @@ -63,8 +50,8 @@ private static boolean isSafeZipEntryName(String entryName) {
* @param pattern the pattern to match
* @return the resources in the order they are found
*/
public static Collection<String> getResources(final Pattern pattern) {
final ArrayList<String> retval = new ArrayList<>();
public Collection<ResourceLocation> getResources(final Pattern pattern) {
final ArrayList<ResourceLocation> retval = new ArrayList<>();
final String classPath = System.getProperty("java.class.path", ".");
final String[] classPathElements = classPath.split(System.getProperty("path.separator"));
for (final String element : classPathElements) {
Expand All @@ -81,29 +68,31 @@ public static Collection<String> getResources(final Pattern pattern) {
* @param pattern the pattern to match
* @return the resources in the order they are found
*/
private static Collection<String> getResources(final String element, final Pattern pattern) {
final ArrayList<String> retval = new ArrayList<>();
final File file = new File(element);
if (file.isDirectory()) {
retval.addAll(getResourcesFromDirectory(file, pattern));
private Collection<ResourceLocation> getResources(final String element, final Pattern pattern) {
final ArrayList<ResourceLocation> retval = new ArrayList<>();
final Path path = Path.of(element);
if (Files.isDirectory(path)) {
retval.addAll(getResourcesFromDirectory(path, pattern, path));
} else {
retval.addAll(getResourcesFromJarFile(file, pattern));
retval.addAll(getResourcesFromJarFile(path, pattern));
}
return retval;
}

/**
* Get all resources from a jar file or a directory that match the given pattern.
*
* @param file the jar file or directory to search in
* @param jarFilePath the jar file or directory to search in
* @param pattern the pattern to match
* @return the resources in the order they are found
*/
private static Collection<String> getResourcesFromJarFile(final File file, final Pattern pattern) {
final ArrayList<String> retval = new ArrayList<>();
private Collection<ResourceLocation> getResourcesFromJarFile(final Path jarFilePath, final Pattern pattern) {
final ArrayList<ResourceLocation> retval = new ArrayList<>();
// Virtual root – no real filesystem access needed
final Path virtualRoot = Paths.get("").toAbsolutePath().normalize();
ZipFile zf;
try {
zf = new ZipFile(file);
zf = new ZipFile(jarFilePath.toFile());
} catch (final ZipException e) {
throw new Error(e);
} catch (final NoSuchFileException e) {
Expand All @@ -115,13 +104,14 @@ private static Collection<String> getResourcesFromJarFile(final File file, final
while (e.hasMoreElements()) {
final ZipEntry ze = e.nextElement();
final String fileName = ze.getName();
if (!isSafeZipEntryName(fileName)) {
if (!isSafeZipEntryName(fileName, virtualRoot)) {
// Optionally log or throw, here we skip unsafe entries
continue;
}
final boolean accept = pattern.matcher(fileName).matches();
if (accept) {
retval.add(fileName);
ResourceLocation location = ResourceLocation.fromPath(fileName);
if (location != null) retval.add(location);
}
}
try {
Expand All @@ -139,26 +129,24 @@ private static Collection<String> getResourcesFromJarFile(final File file, final
* @param pattern the pattern to match
* @return the resources in the order they are found
*/
private static Collection<String> getResourcesFromDirectory(final File directory, final Pattern pattern) {
final ArrayList<String> retval = new ArrayList<>();
final File[] fileList = directory.listFiles();
if (fileList == null) {
return retval;
}
for (final File file : fileList) {
if (file.isDirectory()) {
retval.addAll(getResourcesFromDirectory(file, pattern));
} else {
try {
final String fileName = file.getCanonicalPath();
final boolean accept = pattern.matcher(fileName).matches();
private Collection<ResourceLocation> getResourcesFromDirectory(final Path directory, final Pattern pattern, final Path toplevelPath) {
final ArrayList<ResourceLocation> retval = new ArrayList<>();
try {
Files.list(directory).forEach((path) -> {
if (Files.isDirectory(path)) {
retval.addAll(getResourcesFromDirectory(path, pattern, toplevelPath));
} else {
final Path relativePath = toplevelPath.relativize(path);
final boolean accept = pattern.matcher(relativePath.toString()).matches();
if (accept) {
retval.add(fileName);
ResourceLocation location = ResourceLocation.fromPath(relativePath);
if (location != null) retval.add(location);
}
} catch (final IOException e) {
throw new Error(e);
}
}
});
} catch (IOException e) {
e.printStackTrace();
return retval;
}
return retval;
}
Expand All @@ -170,21 +158,11 @@ private static Collection<String> getResourcesFromDirectory(final File directory
* @param pattern the pattern to match
* @return an array of BufferedReaders for the matching resources
*/
public static BufferedReader[] openResourcesAsReader(Pattern pattern) {
public BufferedReader[] openResourcesAsReader(Pattern pattern) {
List<BufferedReader> readers = new ArrayList<>();
for (String resource : getResources(pattern)) {
for (ResourceLocation resource : getResources(pattern)) {
try {
File file = new File(resource);
InputStream is;
if (file.exists() && file.isFile()) {
is = new FileInputStream(file);
} else {
is = ResourceHelper.class.getResourceAsStream("/" + resource);
if (is == null) {
throw new FileNotFoundException("Resource " + resource + " not found in classpath or filesystem.");
}
}
readers.add(new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)));
readers.add(new BufferedReader(new InputStreamReader(openResourceAsStream(resource))));
} catch (IOException e) {
throw new IllegalStateException(e);
}
Expand All @@ -200,9 +178,9 @@ public static BufferedReader[] openResourcesAsReader(Pattern pattern) {
* @return an InputStream for the resource
* @throws FileNotFoundException if the resource is not found
*/
public static InputStream openResourceAsStream(ResourceLocation location) throws FileNotFoundException {
public InputStream openResourceAsStream(ResourceLocation location) throws FileNotFoundException {
String url = "/" + location.context() + "/" + location.namespace() + "/" + location.resource();
InputStream stream = ResourceHelper.class.getResourceAsStream(url);
InputStream stream = ResourceManager.class.getResourceAsStream(url);
if (stream == null) {
throw new FileNotFoundException(url + " not found in classpath or resources.");
}
Expand All @@ -217,7 +195,7 @@ public static InputStream openResourceAsStream(ResourceLocation location) throws
* @return the content of the resource as a String
* @throws FileNotFoundException if the resource is not found
*/
public static String readResourceCompletely(ResourceLocation location) throws FileNotFoundException {
public String readResourceCompletely(ResourceLocation location) throws FileNotFoundException {
return readResourceCompletely(new BufferedReader(new InputStreamReader(openResourceAsStream(location), StandardCharsets.UTF_8)));
}

Expand All @@ -228,7 +206,7 @@ public static String readResourceCompletely(ResourceLocation location) throws Fi
* @param in the BufferedReader to read from
* @return the content of the BufferedReader as a String
*/
public static String readResourceCompletely(BufferedReader in) {
public String readResourceCompletely(BufferedReader in) {
StringBuilder builder = new StringBuilder();
in.lines().forEach((s) -> {
builder.append(s);
Expand All @@ -246,7 +224,7 @@ public static String readResourceCompletely(BufferedReader in) {
* @return the content read until an empty line is encountered
* @throws IOException if an I/O error occurs
*/
public static String readResourceTillEmptyLine(BufferedReader in) throws IOException {
public String readResourceTillEmptyLine(BufferedReader in) throws IOException {
StringBuilder builder = new StringBuilder();
Stream<String> lines = in.lines();
for (String line : new Iterable<String>() {
Expand All @@ -271,7 +249,7 @@ public Iterator<String> iterator() {
* @param location the ResourceLocation object representing the virtual resource
* @return the content of the virtual resource as a String, or null if not applicable
*/
public static String readVirtualResource(String user, ResourceLocation location) {
public String readVirtualResource(String user, ResourceLocation location) {
if (!location.isVirtual()) {
return null;
} else if (location.namespace().equals("sql")) {
Expand All @@ -281,7 +259,7 @@ public static String readVirtualResource(String user, ResourceLocation location)
}
}

public static Map<String,?> readJsonResourceAsMap(ResourceLocation location) throws IOException {
public Map<String,?> readJsonResourceAsMap(ResourceLocation location) throws IOException {
try (BufferedReader in = new BufferedReader(new InputStreamReader(openResourceAsStream(location), StandardCharsets.UTF_8))) {
Gson gson = new Gson();
java.lang.reflect.Type mapType = new TypeToken<Map<String, Object>>(){}.getType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import java.sql.SQLException;

import de.igslandstuhl.database.server.Server;
import de.igslandstuhl.database.server.resources.ResourceHelper;
import de.igslandstuhl.database.server.resources.ResourceLocation;

/**
Expand Down Expand Up @@ -49,7 +48,7 @@ public static String getSQLQuery(String queryName) {
ResourceLocation location = new ResourceLocation(CONTEXT, QUERIES, queryName + ".sql");
String query;
try {
query = ResourceHelper.readResourceCompletely(location);
query = Server.getInstance().getResourceManager().readResourceCompletely(location);
} catch (FileNotFoundException e) {
throw new SQLCommandNotFoundException(queryName, e);
}
Expand Down Expand Up @@ -90,7 +89,7 @@ private static String getSQLStatement(String type, String object) {
ResourceLocation location = new ResourceLocation(CONTEXT, PUSHES, type + "_" + object + ".sql");
String statement;
try {
statement = ResourceHelper.readResourceCompletely(location);
statement = Server.getInstance().getResourceManager().readResourceCompletely(location);
} catch (FileNotFoundException e) {
throw new SQLCommandNotFoundException(type + "_" + object, e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import java.sql.Statement;
import java.util.regex.Pattern;

import de.igslandstuhl.database.server.resources.ResourceHelper;
import de.igslandstuhl.database.server.Server;
import de.igslandstuhl.database.utils.TrackingReadWriteLock;

/**
Expand Down Expand Up @@ -47,9 +47,9 @@ public Connection getSQLConnection() {
private void createTables(PreparedStatementSupplier supplier) throws SQLException {
lock.writeLock().lock();
try {
for (BufferedReader in : ResourceHelper.openResourcesAsReader(Pattern.compile(".*tables.+\\.sql"))) {
for (BufferedReader in : Server.getInstance().getResourceManager().openResourcesAsReader(Pattern.compile(".*tables.+\\.sql"))) {
try (in) {
String request = ResourceHelper.readResourceCompletely(in);
String request = Server.getInstance().getResourceManager().readResourceCompletely(in);
supplier.executeUpdate(request);
} catch (IOException e) {
throw new IllegalStateException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import java.util.Map;

import de.igslandstuhl.database.api.User;
import de.igslandstuhl.database.server.resources.ResourceHelper;
import de.igslandstuhl.database.server.Server;
import de.igslandstuhl.database.server.resources.ResourceLocation;

/**
Expand Down Expand Up @@ -70,7 +70,7 @@ private AccessManager() {
String[] teacherLocations = {};
String[] adminLocations = {"students", "teachers", "classes"};
try {
Map<String, ?> pathData = ResourceHelper.readJsonResourceAsMap(metaLocation);
Map<String, ?> pathData = Server.getInstance().getResourceManager().readJsonResourceAsMap(metaLocation);
List<String> publicSpacesList = (List<String>) pathData.get("public_spaces");
List<String> publicLocationsList = (List<String>) pathData.get("public_locations");
List<String> userLocationsList = (List<String>) pathData.get("user_locations");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.util.Map;

import de.igslandstuhl.database.Registry;
import de.igslandstuhl.database.server.resources.ResourceHelper;
import de.igslandstuhl.database.server.Server;
import de.igslandstuhl.database.server.resources.ResourceLocation;
import de.igslandstuhl.database.server.webserver.requests.RequestType;

Expand All @@ -16,7 +16,7 @@ public static void registerPath(String path, RequestType type, String handlerTyp
public static void registerPaths() throws IOException {
if (Registry.webPathRegistry().stream().count() > 0) return; // already registered
ResourceLocation metaLocation = new ResourceLocation("meta", "paths", "get_paths.json");
Map<String, ?> pathData = ResourceHelper.readJsonResourceAsMap(metaLocation);
Map<String, ?> pathData = Server.getInstance().getResourceManager().readJsonResourceAsMap(metaLocation);
pathData.keySet().forEach((path) -> {
@SuppressWarnings("unchecked")
Map<String, ?> pathInfo = (Map<String, ?>) pathData.get(path);
Expand Down
Loading
Loading