diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cadb57b73f3b6c3c8390f2768cdd98575cf0089..d12b8723bb97977d2aefab83fbd174be51dc83bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* New server property `SERVER_SIDE_SCRIPTING_BIN_DIRS` which accepts a comma or + space separated list as values. The server looks for scripts in all + directories in the order or the list and uses the first matching file. + ### Changed ### Deprecated +* `SERVER_SIDE_SCRIPTING_BIN_DIR` property is deprecated. + `SERVER_SIDE_SCRIPTING_BIN_DIRS` should be used instead (note the plural + form!) + ### Removed ### Fixed diff --git a/conf/core/server.conf b/conf/core/server.conf index 73a7bc871c47409f48dd724988818ab210e0ab87..1eb96c7bf2dd2a5a216c81222e548a6155636091 100644 --- a/conf/core/server.conf +++ b/conf/core/server.conf @@ -12,9 +12,10 @@ SERVER_NAME=CaosDB Server # The following paths are relative to the working directory of the server. # -------------------------------------------------- -# The location of the server side scripting binaries. +# The location(s) of the server side scripting binaries. # Put your executable python scripts here, if they need to be called from the scripting API. -SERVER_SIDE_SCRIPTING_BIN_DIR=./scripting/bin/ +# The value is a comma or space separated list or a single directory +SERVER_SIDE_SCRIPTING_BIN_DIRS=./scripting/bin/ # Working directory of the server side scripting API. # On execution of binaries and scripts the server will create a corresponding working directory in this folder. diff --git a/src/main/java/org/caosdb/server/ServerProperties.java b/src/main/java/org/caosdb/server/ServerProperties.java index dd12add7f20de16d74b2cda907e543b7f5ddef16..3172a3e88e7790580af1b7865c024fa2b40c017b 100644 --- a/src/main/java/org/caosdb/server/ServerProperties.java +++ b/src/main/java/org/caosdb/server/ServerProperties.java @@ -117,6 +117,7 @@ public class ServerProperties extends Properties { public static final String KEY_SERVER_OWNER = "SERVER_OWNER"; public static final String KEY_SERVER_SIDE_SCRIPTING_BIN_DIR = "SERVER_SIDE_SCRIPTING_BIN_DIR"; + public static final String KEY_SERVER_SIDE_SCRIPTING_BIN_DIRS = "SERVER_SIDE_SCRIPTING_BIN_DIRS"; public static final String KEY_SERVER_SIDE_SCRIPTING_HOME_DIR = "SERVER_SIDE_SCRIPTING_HOME_DIR"; public static final String KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR = "SERVER_SIDE_SCRIPTING_WORKING_DIR"; diff --git a/src/main/java/org/caosdb/server/scripting/ScriptingUtils.java b/src/main/java/org/caosdb/server/scripting/ScriptingUtils.java index 5bb9498944f02c3cc2bd79429dab10868299200f..c9a0b522d949a4ba3c36ae4f867661263a0185d3 100644 --- a/src/main/java/org/caosdb/server/scripting/ScriptingUtils.java +++ b/src/main/java/org/caosdb/server/scripting/ScriptingUtils.java @@ -23,7 +23,8 @@ package org.caosdb.server.scripting; import java.io.File; -import java.nio.file.Path; +import java.io.IOException; +import java.util.ArrayList; import org.caosdb.server.CaosDBServer; import org.caosdb.server.ServerProperties; import org.caosdb.server.entity.Message; @@ -33,25 +34,48 @@ import org.caosdb.server.utils.Utils; public class ScriptingUtils { - private File bin; + private File[] bin_dirs; private File working; public ScriptingUtils() { - this.bin = - new File( - CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIR)); + ArrayList<File> new_bin_dirs = new ArrayList<>(); + String bin_dirs_str = + CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIRS); + if (bin_dirs_str == null) { + // fall-back for old server property + bin_dirs_str = + CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIR); + } + + // split and process + if (bin_dirs_str != null) { + for (String dir : bin_dirs_str.split("\\s+|\\s*,\\s*")) { + + File bin; + try { + bin = new File(dir).getCanonicalFile(); + } catch (IOException e) { + throw new ConfigurationException( + "Scripting bin dir `" + dir + "` cannot be resolved to a real path."); + } + if (!bin.exists()) { + bin.mkdirs(); + } + if (!bin.isDirectory()) { + throw new ConfigurationException( + "The ServerProperty `" + + ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIRS + + "` must point to directories"); + } + new_bin_dirs.add(bin); + } + } + + bin_dirs = new_bin_dirs.toArray(new File[new_bin_dirs.size()]); + this.working = new File( CaosDBServer.getServerProperty(ServerProperties.KEY_SERVER_SIDE_SCRIPTING_WORKING_DIR)); - if (!bin.exists()) { - bin.mkdirs(); - } - if (!bin.isDirectory()) { - throw new ConfigurationException( - "The ServerProperty `" - + ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIR - + "` must point to a directory"); - } if (!working.exists()) { working.mkdirs(); @@ -64,21 +88,46 @@ public class ScriptingUtils { } } - public File getScriptFile(final String command) { - final Path script = bin.toPath().resolve(command); - return script.toFile(); - } + /** + * Get the script file by the relative path. + * + * <p>Run through all registered bin_dirs and try to resolve the command relative to them. The + * first matching file is used. When it is not executable throw a + * SERVER_SIDE_SCRIPT_NOT_EXECUTABLE message. When no matching file exists throw a + * SERVER_SIDE_SCRIPT_DOES_NOT_EXIST message. + * + * @param command The relative path + * @return The script File object. + * @throws Message + */ + public File getScriptFile(final String command) throws Message { + for (File bin_dir : bin_dirs) { + File script = bin_dir.toPath().resolve(command).toFile(); - public void checkScriptExists(final String command) throws Message { - if (!getScriptFile(command).exists()) { - throw ServerMessages.SERVER_SIDE_SCRIPT_DOES_NOT_EXIST; - } - } + try { + script = script.getCanonicalFile(); + if (!script.toPath().startsWith(bin_dir.toPath())) { + // not below the allowed directory tree + continue; + } + } catch (IOException e) { + // cannot be resolved to canonical file - we treat it as non-existing. + continue; + } + + if (!script.exists()) { + // doesn't exist. + continue; + } + + if (!script.canExecute()) { + throw ServerMessages.SERVER_SIDE_SCRIPT_NOT_EXECUTABLE; + } - public void checkScriptExecutable(final String command) throws Message { - if (!getScriptFile(command).canExecute()) { - throw ServerMessages.SERVER_SIDE_SCRIPT_NOT_EXECUTABLE; + // we found it! + return script; } + throw ServerMessages.SERVER_SIDE_SCRIPT_DOES_NOT_EXIST; } public File getTmpWorkingDir() { diff --git a/src/main/java/org/caosdb/server/scripting/ServerSideScriptingCaller.java b/src/main/java/org/caosdb/server/scripting/ServerSideScriptingCaller.java index 2ec19a426fdd5a7f0cb6438b86f73a3dc93f55cd..4f5f81e248e17f6344d8523286828ce7142c529a 100644 --- a/src/main/java/org/caosdb/server/scripting/ServerSideScriptingCaller.java +++ b/src/main/java/org/caosdb/server/scripting/ServerSideScriptingCaller.java @@ -54,6 +54,7 @@ public class ServerSideScriptingCaller { public static final Integer STARTED = -1; private final String[] commandLine; private final int timeoutMs; + private String absoluteScriptPath = null; private ScriptingUtils utils; private List<FileProperties> files; private File workingDir; @@ -122,7 +123,7 @@ public class ServerSideScriptingCaller { /** Does some final preparation, then calls the script and cleans up. */ public int invoke() throws Message { try { - checkCommandLine(commandLine); + this.absoluteScriptPath = getAbsoluteScriptPath(commandLine); try { createWorkingDir(); putFilesInWorkingDir(files); @@ -135,7 +136,8 @@ public class ServerSideScriptingCaller { } try { - return callScript(); + code = callScript(this.absoluteScriptPath, this.commandLine, this.authToken, this.env); + return code; } catch (TimeoutException e) { throw ServerMessages.SERVER_SIDE_SCRIPT_TIMEOUT; } catch (final Throwable e) { @@ -147,9 +149,17 @@ public class ServerSideScriptingCaller { } } - void checkCommandLine(String[] commandLine) throws Message { - utils.checkScriptExists(commandLine[0]); - utils.checkScriptExecutable(commandLine[0]); + /** + * Returns the absolute script path. + * + * <p>Throws Message when the script does not exist or when the script is not executable. + * + * @param commandLine + * @return The absolute script path. + * @throws Message + */ + String getAbsoluteScriptPath(String[] commandLine) throws Message { + return utils.getScriptFile(commandLine[0]).getAbsolutePath(); } void putFilesInWorkingDir(final Collection<FileProperties> files) @@ -243,10 +253,6 @@ public class ServerSideScriptingCaller { if (pwd.exists()) FileUtils.forceDelete(pwd); } - String makeCallAbsolute(String call) { - return utils.getScriptFile(call).getAbsolutePath(); - } - /** @fixme Should be injected into environment instead. Will be changed in v0.4 of SSS-API */ String[] injectAuthToken(String[] commandLine) { String[] newCommandLine = new String[commandLine.length + 1]; @@ -258,14 +264,31 @@ public class ServerSideScriptingCaller { return newCommandLine; } - int callScript() throws IOException, InterruptedException, TimeoutException { + /** + * Call the script. + * + * <p>The absoluteScriptPath is called with all remaining parameters from the commandLine arrays, + * an optional additional authToken and environment variables. + * + * @param absoluteScriptPath + * @param commandLine + * @param authToken + * @param env - environment variables + * @return the exit code of the script call + * @throws IOException + * @throws InterruptedException + * @throws TimeoutException + */ + int callScript( + String absoluteScriptPath, String[] commandLine, Object authToken, Map<String, String> env) + throws IOException, InterruptedException, TimeoutException { String[] effectiveCommandLine; if (authToken != null) { - effectiveCommandLine = injectAuthToken(getCommandLine()); + effectiveCommandLine = injectAuthToken(commandLine); } else { - effectiveCommandLine = Arrays.copyOf(getCommandLine(), getCommandLine().length); + effectiveCommandLine = Arrays.copyOf(commandLine, commandLine.length); } - effectiveCommandLine[0] = makeCallAbsolute(effectiveCommandLine[0]); + effectiveCommandLine[0] = absoluteScriptPath; final ProcessBuilder pb = new ProcessBuilder(effectiveCommandLine); // inject environment variables @@ -278,7 +301,7 @@ public class ServerSideScriptingCaller { pb.redirectError(Redirect.to(getStdErrFile())); pb.directory(getTmpWorkingDir()); - code = STARTED; + int code = STARTED; final TimeoutProcess process = new TimeoutProcess(pb.start(), getTimeoutMs()); code = process.waitFor(); diff --git a/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java b/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java index 28fdd55e78db1a2142b5eac86327617b8f371089..61be0c5c3a06319dbeb826a79ddf1c6e43a0d672 100644 --- a/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java +++ b/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java @@ -39,6 +39,7 @@ import org.apache.commons.io.FileUtils; import org.caosdb.CaosDBTestClass; import org.caosdb.server.CaosDBException; import org.caosdb.server.CaosDBServer; +import org.caosdb.server.ServerProperties; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.entity.FileProperties; import org.caosdb.server.entity.Message; @@ -68,6 +69,10 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { @BeforeClass public static void setupTestFolder() throws IOException { + CaosDBServer.getServerProperties() + .setProperty( + ServerProperties.KEY_SERVER_SIDE_SCRIPTING_BIN_DIRS, testFolder.getAbsolutePath()); + FileUtils.forceDeleteOnExit(testFolder); FileUtils.forceDeleteOnExit(testFile); FileUtils.forceDeleteOnExit(testExecutable); @@ -305,7 +310,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { testExeExit(1); - assertEquals(1, caller.callScript()); + assertEquals(1, caller.callScript(cmd[0], cmd, null, emptyEnv)); } @Test @@ -319,7 +324,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { testExeExit(0); - assertEquals(0, caller.callScript()); + assertEquals(0, caller.callScript(cmd[0], cmd, null, emptyEnv)); } @Test @@ -387,7 +392,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { testSleep(10); this.exception.expect(TimeoutException.class); - caller.callScript(); + caller.callScript(cmd[0], cmd, null, emptyEnv); } @Test @@ -418,7 +423,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { throws FileNotFoundException, CaosDBException {} @Override - public int callScript() { + public int callScript( + String cmd, String[] cmdLine, Object authToken, Map<String, String> env) { return 0; } @@ -456,7 +462,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { } @Override - public int callScript() { + public int callScript( + String cmd, String[] cmdLine, Object authToken, Map<String, String> env) { return 0; } @@ -496,7 +503,9 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { public void createTmpHomeDir() throws Exception {} @Override - public int callScript() throws IOException { + public int callScript( + String cmd, String[] cmdLine, Object authToken, Map<String, String> env) + throws IOException { throw new IOException(); } @@ -532,7 +541,8 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { throws FileNotFoundException, CaosDBException {} @Override - public int callScript() { + public int callScript( + String cmd, String[] cmdLine, Object authToken, Map<String, String> env) { return 0; } @@ -562,7 +572,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { testPrintArgsToStdErr(); - caller.callScript(); + caller.callScript(cmd[0], cmd, "authToken", emptyEnv); assertEquals( "--auth-token=authToken opt1 opt2\n", FileUtils.readFileToString(caller.getStdErrFile())); @@ -579,7 +589,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { testPrintArgsToStdOut(); - caller.callScript(); + caller.callScript(cmd[0], cmd, "authToken", emptyEnv); assertEquals( "--auth-token=authToken opt1 opt2\n", FileUtils.readFileToString(caller.getStdOutFile())); @@ -599,7 +609,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass { preparePrintEnv("TEST"); - caller.callScript(); + caller.callScript(cmd[0], cmd, null, env); assertEquals("-testcontent-\n", caller.getStdOut()); caller.cleanup();