updated control access

This commit is contained in:
Quality System Admin
2025-10-16 02:36:32 +03:00
parent 50c791e242
commit c96039542d
266 changed files with 32656 additions and 9 deletions

View File

@@ -0,0 +1,161 @@
package qz.installer.provision;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.Phase;
import qz.build.provision.params.types.Script;
import qz.build.provision.params.types.Software;
import qz.common.Constants;
import qz.installer.provision.invoker.*;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import static qz.common.Constants.*;
import static qz.utils.FileUtilities.*;
public class ProvisionInstaller {
protected static final Logger log = LogManager.getLogger(ProvisionInstaller.class);
private ArrayList<Step> steps;
static {
// Populate variables for scripting environment
ShellUtilities.addEnvp("APP_TITLE", ABOUT_TITLE,
"APP_VERSION", VERSION,
"APP_ABBREV", PROPS_FILE,
"APP_VENDOR", ABOUT_COMPANY,
"APP_VENDOR_ABBREV", DATA_DIR,
"APP_ARCH", SystemUtilities.getArch(),
"APP_OS", SystemUtilities.getOs(),
"APP_DIR", SystemUtilities.getAppPath(),
"APP_USER_DIR", USER_DIR,
"APP_SHARED_DIR", SHARED_DIR);
}
public ProvisionInstaller(Path relativePath) throws IOException, JSONException {
this(relativePath, relativePath.resolve(Constants.PROVISION_FILE).toFile());
}
public ProvisionInstaller(Path relativePath, File jsonFile) throws IOException, JSONException {
if(!jsonFile.exists()) {
log.info("Provision file not found '{}', skipping", jsonFile);
this.steps = new ArrayList<>();
return;
}
this.steps = parse(FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8), relativePath);
}
/**
* Package private for internal testing only
* Assumes files located in ./resources/ subdirectory
*/
ProvisionInstaller(Class relativeClass, InputStream in) throws IOException, JSONException {
this(relativeClass, IOUtils.toString(in, StandardCharsets.UTF_8));
}
/**
* Package private for internal testing only
* Assumes files located in ./resources/ subdirectory
*/
ProvisionInstaller(Class relativeClass, String jsonData) throws JSONException {
this.steps = parse(jsonData, relativeClass);
}
public void invoke(Phase phase) {
for(Step step : this.steps) {
if(phase == null || step.getPhase() == phase) {
try {
invokeStep(step);
}
catch(Exception e) {
log.error("[PROVISION] Provisioning step failed '{}'", step, e);
}
}
}
}
public void invoke() {
invoke(null);
}
private static ArrayList<Step> parse(String jsonData, Object relativeObject) throws JSONException {
return parse(new JSONArray(jsonData), relativeObject);
}
private boolean invokeStep(Step step) throws Exception {
if(Os.matchesHost(step.getOs())) {
log.info("[PROVISION] Invoking step '{}'", step.toString());
} else {
log.info("[PROVISION] Skipping step for different OS '{}'", step.toString());
return false;
}
Invokable invoker;
switch(step.getType()) {
case CA:
invoker = new CaInvoker(step, PropertyInvoker.getProperties(step));
break;
case CERT:
invoker = new CertInvoker(step);
break;
case CONF:
invoker = new ConfInvoker(step);
break;
case SCRIPT:
invoker = new ScriptInvoker(step);
break;
case SOFTWARE:
invoker = new SoftwareInvoker(step);
break;
case REMOVER:
invoker = new RemoverInvoker(step);
break;
case RESOURCE:
invoker = new ResourceInvoker(step);
break;
case PREFERENCE:
invoker = new PropertyInvoker(step, PropertyInvoker.getPreferences(step));
break;
case PROPERTY:
invoker = new PropertyInvoker(step, PropertyInvoker.getProperties(step));
break;
default:
throw new UnsupportedOperationException("Type " + step.getType() + " is not yet supported.");
}
return invoker.invoke();
}
public ArrayList<Step> getSteps() {
return steps;
}
private static ArrayList<Step> parse(JSONArray jsonArray, Object relativeObject) throws JSONException {
ArrayList<Step> steps = new ArrayList<>();
for(int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonStep = jsonArray.getJSONObject(i);
try {
steps.add(Step.parse(jsonStep, relativeObject));
} catch(Exception e) {
log.warn("[PROVISION] Unable to add step '{}'", jsonStep, e);
}
}
return steps;
}
public static boolean shouldBeExecutable(Path path) {
return Script.parse(path) != null || Software.parse(path) != Software.UNKNOWN;
}
}

View File

@@ -0,0 +1,49 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.PropertyHelper;
import qz.utils.ArgValue;
import qz.utils.FileUtilities;
import java.io.File;
import java.io.IOException;
/**
* Combines ResourceInvoker and PropertyInvoker to deploy a file and set a property to its deployed path
*/
public class CaInvoker extends InvokableResource {
Step step;
PropertyHelper properties;
public CaInvoker(Step step, PropertyHelper properties) {
this.step = step;
this.properties = properties;
}
@Override
public boolean invoke() throws IOException {
// First, write our cert file
File caCert = dataToFile(step);
if(caCert == null) {
return false;
}
// Next, handle our property step
Step propsStep = step.clone();
// If the property already exists, snag it
String key = ArgValue.AUTHCERT_OVERRIDE.getMatch();
String value = caCert.getPath();
if (properties.containsKey(key)) {
value = properties.getProperty(key) + FileUtilities.FILE_SEPARATOR + value;
}
propsStep.setData(String.format("%s=%s", key, value));
if (new PropertyInvoker(propsStep, properties).invoke()) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,26 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.Constants;
import qz.utils.FileUtilities;
import java.io.File;
import static qz.utils.ArgParser.ExitStatus.*;
public class CertInvoker extends InvokableResource {
private Step step;
public CertInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
File cert = dataToFile(step);
if(cert == null) {
return false;
}
return FileUtilities.addToCertList(Constants.ALLOW_FILE, cert) == SUCCESS;
}
}

View File

@@ -0,0 +1,46 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.PropertyHelper;
import qz.utils.SystemUtilities;
import java.util.AbstractMap;
public class ConfInvoker extends PropertyInvoker {
public ConfInvoker(Step step) {
super(step, new PropertyHelper(calculateConfPath(step)));
}
public static String calculateConfPath(Step step) {
String relativePath = step.getArgs().get(0);
if(SystemUtilities.isMac()) {
return SystemUtilities.getJarParentPath().
resolve("../PlugIns/Java.runtime/Contents/Home/conf").
resolve(relativePath).
normalize()
.toString();
} else {
return SystemUtilities.getJarParentPath()
.resolve("runtime/conf")
.resolve(relativePath)
.normalize()
.toString();
}
}
@Override
public boolean invoke() {
Step step = getStep();
// Java uses the same "|" delimiter as we do, only parse one property at a time
AbstractMap.SimpleEntry<String, String> pair = parsePropertyPair(step, step.getData());
if (!pair.getValue().isEmpty()) {
properties.setProperty(pair);
if (properties.save()) {
log.info("Successfully provisioned '1' '{}'", step.getType());
return true;
}
log.error("An error occurred saving properties '{}' to file", step.getData());
}
return false;
}
}

View File

@@ -0,0 +1,10 @@
package qz.installer.provision.invoker;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public interface Invokable {
Logger log = LogManager.getLogger(Invokable.class);
boolean invoke() throws Exception;
}

View File

@@ -0,0 +1,63 @@
package qz.installer.provision.invoker;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.build.provision.Step;
import qz.build.provision.params.Type;
import qz.common.Constants;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
public abstract class InvokableResource implements Invokable {
static final Logger log = LogManager.getLogger(InvokableResource.class);
public static File dataToFile(Step step) throws IOException {
Path resourcePath = Paths.get(step.getData());
if(resourcePath.isAbsolute() || step.usingPath()) {
return pathResourceToFile(step);
}
if(step.usingClass()) {
return classResourceToFile(step);
}
return null;
}
/**
* Resolves the resource directly from file
*/
private static File pathResourceToFile(Step step) {
String resourcePath = step.getData();
Path dataPath = Paths.get(resourcePath);
return dataPath.isAbsolute() ? dataPath.toFile() : step.getRelativePath().resolve(resourcePath).toFile();
}
/**
* Copies resource from JAR to a temp file for use in installation
*/
private static File classResourceToFile(Step step) throws IOException {
// Resource may be inside the jar
InputStream in = step.getRelativeClass().getResourceAsStream("resources/" + step.getData());
if(in == null) {
log.warn("Resource '{}' is missing, skipping step", step.getData());
return null;
}
String suffix = "_" + Paths.get(step.getData()).getFileName().toString();
File destination = File.createTempFile(Constants.DATA_DIR + "_provision_", suffix);
Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
IOUtils.closeQuietly(in);
// Set scripts executable
if(step.getType() == Type.SCRIPT && !SystemUtilities.isWindows()) {
destination.setExecutable(true, false);
}
return destination;
}
}

View File

@@ -0,0 +1,99 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.Constants;
import qz.common.PropertyHelper;
import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
public class PropertyInvoker implements Invokable {
private Step step;
PropertyHelper properties;
public PropertyInvoker(Step step, PropertyHelper properties) {
this.step = step;
this.properties = properties;
}
public boolean invoke() {
HashMap<String, String> pairs = parsePropertyPairs(step);
if (!pairs.isEmpty()) {
for(Map.Entry<String, String> pair : pairs.entrySet()) {
properties.setProperty(pair);
}
if (properties.save()) {
log.info("Successfully provisioned '{}' '{}'", pairs.size(), step.getType());
return true;
}
log.error("An error occurred saving properties '{}' to file", step.getData());
}
return false;
}
public static PropertyHelper getProperties(Step step) {
File propertiesFile;
if(step.getRelativePath() != null) {
// Assume qz-tray.properties is one directory up from provision folder
// required to prevent installing to payload
propertiesFile = step.getRelativePath().getParent().resolve(Constants.PROPS_FILE + ".properties").toFile();
} else {
// If relative path isn't set, fallback to the jar's parent path
propertiesFile = SystemUtilities.getJarParentPath(".").resolve(Constants.PROPS_FILE + ".properties").toFile();
}
log.info("Provisioning '{}' to properties file: '{}'", step.getData(), propertiesFile);
return new PropertyHelper(propertiesFile);
}
public static PropertyHelper getPreferences(Step step) {
return new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
}
public static HashMap<String, String> parsePropertyPairs(Step step) {
HashMap<String, String> pairs = new HashMap<>();
if(step.getData() != null && !step.getData().trim().isEmpty()) {
String[] props = step.getData().split("\\|");
for(String prop : props) {
AbstractMap.SimpleEntry<String,String> pair = parsePropertyPair(step, prop);
if (pair != null) {
if(pairs.get(pair.getKey()) != null) {
log.warn("Property {} already exists, replacing [before: {}, after: {}] ",
pair.getKey(), pairs.get(pair.getKey()), pair.getValue());
}
pairs.put(pair.getKey(), pair.getValue());
}
}
} else {
log.error("Skipping Step '{}', Data is null or empty", step.getType());
}
return pairs;
}
public static AbstractMap.SimpleEntry<String, String> parsePropertyPair(Step step, String prop) {
if(prop.contains("=")) {
String[] pair = prop.split("=", 2);
if (!pair[0].trim().isEmpty()) {
if (!pair[1].trim().isEmpty()) {
return new AbstractMap.SimpleEntry<>(pair[0], pair[1]);
} else {
log.warn("Skipping '{}' '{}', property value is malformed", step.getType(), prop);
}
} else {
log.warn("Skipping '{}' '{}', property name is malformed", step.getType(), prop);
}
} else {
log.warn("Skipping '{}' '{}', property is malformed", step.getType(), prop);
}
return null;
}
public Step getStep() {
return step;
}
}

View File

@@ -0,0 +1,100 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.types.Remover;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
public class RemoverInvoker extends InvokableResource {
private Step step;
private String aboutTitle; // e.g. "QZ Tray"
private String propsFile; // e.g. "qz-tray"
private String dataDir; // e.g. "qz"
public RemoverInvoker(Step step) {
this.step = step;
Remover remover = Remover.parse(step.getData());
if(remover == Remover.CUSTOM) {
// Fields are comma delimited in the data field
parseCustomFromData(step.getData());
} else {
aboutTitle = remover.getAboutTitle();
propsFile = remover.getPropsFile();
dataDir = remover.getDataDir();
}
}
@Override
public boolean invoke() throws Exception {
ArrayList<String> command = getRemoveCommand();
if(command.size() == 0) {
log.info("An existing installation of '{}' was not found. Skipping.", aboutTitle);
return true;
}
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
if(!success) {
log.error("An error occurred invoking [{}]", step.getData());
}
return success;
}
public void parseCustomFromData(String data) {
String[] parts = data.split(",");
aboutTitle = parts[0].trim();
propsFile = parts[1].trim();
dataDir = parts[2].trim();
}
/**
* Returns the installer command (including the installer itself and if needed, arguments) to
* invoke the installer file
*/
public ArrayList<String> getRemoveCommand() {
ArrayList<String> removeCmd = new ArrayList<>();
Os os = SystemUtilities.getOs();
switch(os) {
case WINDOWS:
Path win = Paths.get(System.getenv("PROGRAMFILES"))
.resolve(aboutTitle)
.resolve("uninstall.exe");
if(win.toFile().exists()) {
removeCmd.add(win.toString());
removeCmd.add("/S");
break;
}
case MAC:
Path legacy = Paths.get("/Applications")
.resolve(aboutTitle + ".app")
.resolve("Contents")
.resolve("uninstall");
Path mac = Paths.get("/Applications")
.resolve(aboutTitle + ".app")
.resolve("Contents")
.resolve("Resources")
.resolve("uninstall");
if(legacy.toFile().exists()) {
removeCmd.add(legacy.toString());
} else if(mac.toFile().exists()) {
removeCmd.add(mac.toString());
}
break;
default:
Path linux = Paths.get("/opt")
.resolve(propsFile)
.resolve("uninstall");
if(linux.toFile().exists()) {
removeCmd.add(linux.toString());
}
}
return removeCmd;
}
}

View File

@@ -0,0 +1,19 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
/**
* Stub class for deploying an otherwise "action-less" resource, only to be used by other tasks
*/
public class ResourceInvoker extends InvokableResource {
private Step step;
public ResourceInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
return dataToFile(step) != null;
}
}

View File

@@ -0,0 +1,77 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.types.Script;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.util.ArrayList;
public class ScriptInvoker extends InvokableResource {
private Step step;
public ScriptInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
File script = dataToFile(step);
if(script == null) {
return false;
}
Script engine = Script.parse(step.getData());
ArrayList<String> command = getInterpreter(engine);
if(command.isEmpty() && SystemUtilities.isWindows()) {
log.warn("No interpreter found for {}, skipping", step.getData());
return false;
}
command.add(script.toString());
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
if(!success) {
log.error("An error occurred invoking [{}]", step.getData());
}
return success;
}
/**
* Returns the interpreter command (and if needed, arguments) to invoke the script file
*
* An empty array will fall back to Unix "shebang" notation, e.g. #!/usr/bin/python3
* which will allow the OS to select the correct interpreter for the given file
*
* No special attention is given to "shebang", behavior may differ between OSs
*/
private static ArrayList<String> getInterpreter(Script engine) {
ArrayList<String> interpreter = new ArrayList<>();
Os osType = SystemUtilities.getOs();
switch(engine) {
case PS1:
if(osType == Os.WINDOWS) {
interpreter.add("powershell.exe");
} else if(osType == Os.MAC) {
interpreter.add("/usr/local/bin/pwsh");
} else {
interpreter.add("pwsh");
}
interpreter.add("-File");
break;
case PY:
interpreter.add(osType == Os.WINDOWS ? "python3.exe" : "python3");
break;
case BAT:
interpreter.add(osType == Os.WINDOWS ? "cmd.exe" : "wineconsole");
break;
case RB:
interpreter.add(osType == Os.WINDOWS ? "ruby.exe" : "ruby");
break;
case SH:
default:
// Allow the environment to parse it from the shebang at invocation time
}
return interpreter;
}
}

View File

@@ -0,0 +1,87 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.types.Software;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class SoftwareInvoker extends InvokableResource {
private Step step;
public SoftwareInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
File payload = dataToFile(step);
if(payload == null) {
return false;
}
Software installer = Software.parse(step.getData());
ArrayList<String> command = getInstallCommand(installer, step.getArgs(), payload);
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]), payload.getParentFile());
if(!success) {
log.error("An error occurred invoking [{}]", step.getData());
}
return success;
}
/**
* Returns the installer command (including the installer itself and if needed, arguments) to
* invoke the installer file
*/
public ArrayList<String> getInstallCommand(Software installer, List<String> args, File payload) {
ArrayList<String> interpreter = new ArrayList<>();
Os os = SystemUtilities.getOs();
switch(installer) {
case EXE:
if(!SystemUtilities.isWindows()) {
interpreter.add("wine");
}
// Executable on its own
interpreter.add(payload.toString());
interpreter.addAll(args); // Assume exe args come after payload
break;
case MSI:
interpreter.add(os == Os.WINDOWS ? "msiexec.exe" : "msiexec");
interpreter.add("/i"); // Assume standard install
interpreter.add(payload.toString());
interpreter.addAll(args); // Assume msiexec args come after payload
break;
case PKG:
if(os == Os.MAC) {
interpreter.add("installer");
interpreter.addAll(args); // Assume installer args come before payload
interpreter.add("-package");
interpreter.add(payload.toString());
interpreter.add("-target");
interpreter.add("/"); // Assume we don't want this on a removable volume
} else {
throw new UnsupportedOperationException("PKG is not yet supported on this platform");
}
break;
case DMG:
// DMG requires "hdiutil attach", but the mount point is unknown
throw new UnsupportedOperationException("DMG is not yet supported");
case RUN:
if(SystemUtilities.isWindows()) {
interpreter.add("bash");
interpreter.add("-c");
}
interpreter.add(payload.toString());
interpreter.addAll(args); // Assume run args come after payload
// Executable on its own
break;
default:
// We'll try to parse it from the shebang just before invocation time
}
return interpreter;
}
}